Browse Source

feat(issues): GA search shortcuts (#53903)

Scott Cooper 1 year ago
parent
commit
d25d3551c8

+ 0 - 5
static/app/utils/analytics/issueAnalyticsEvents.tsx

@@ -161,9 +161,6 @@ export type IssueEventParameters = {
     did_assign_suggestion: boolean;
     assigned_suggestion_reason?: string;
   };
-  'issues_stream.issue_category_dropdown_changed': {
-    category: string;
-  };
   'issues_stream.paginate': {
     direction: string;
   };
@@ -247,8 +244,6 @@ export const issueEventMap: Record<IssueEventKey, string | null> = {
   'issues_stream.issue_assigned': 'Assigned Issue from Issues Stream',
   'issues_stream.sort_changed': 'Changed Sort on Issues Stream',
   'issues_stream.paginate': 'Paginate Issues Stream',
-  'issues_stream.issue_category_dropdown_changed':
-    'Issues Stream: Issue Category Dropdown Changed',
   'issue.shared_publicly': 'Issue Shared Publicly',
   'issue_group_details.stack_traces.setup_source_maps_alert.clicked':
     'Issue Group Details: Setup Source Maps Alert Clicked',

+ 13 - 20
static/app/utils/withIssueTags.spec.tsx

@@ -1,6 +1,6 @@
 import {act, render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
 
-import {SearchGroup} from 'sentry/components/smartSearchBar/types';
+import type {SearchGroup} from 'sentry/components/smartSearchBar/types';
 import MemberListStore from 'sentry/stores/memberListStore';
 import TagStore from 'sentry/stores/tagStore';
 import TeamStore from 'sentry/stores/teamStore';
@@ -16,7 +16,11 @@ function MyComponent(props: MyComponentProps) {
       {'is: ' + props.tags?.is?.values?.[0]}
       {'mechanism: ' + props.tags?.mechanism?.values?.join(', ')}
       {'bookmarks: ' + props.tags?.bookmarks?.values?.join(', ')}
-      {'assigned: ' + props.tags?.assigned?.values?.join(', ')}
+      {'assigned: ' +
+        (props.tags?.assigned?.values as SearchGroup[])
+          .flatMap(x => x.children)
+          .map(x => x.desc)
+          ?.join(', ')}
       {'stack filename: ' + props.tags?.['stack.filename'].name}
     </div>
   );
@@ -90,16 +94,7 @@ describe('withIssueTags HoC', function () {
   });
 
   it('groups assignees and puts suggestions first', function () {
-    const Container = withIssueTags(({tags}: MyComponentProps) => (
-      <div>
-        {(tags?.assigned?.values as SearchGroup[])?.map(searchGroup => (
-          <div data-test-id={searchGroup.title} key={searchGroup.title}>
-            {searchGroup.children?.map(item => item.desc).join(', ')}
-          </div>
-        ))}
-      </div>
-    ));
-    const organization = TestStubs.Organization({features: ['issue-search-shortcuts']});
+    const Container = withIssueTags(MyComponent);
     TeamStore.loadInitialData([
       TestStubs.Team({id: 1, slug: 'best-team', name: 'Best Team', isMember: true}),
       TestStubs.Team({id: 2, slug: 'worst-team', name: 'Worst Team', isMember: false}),
@@ -108,16 +103,14 @@ describe('withIssueTags HoC', function () {
       TestStubs.User(),
       TestStubs.User({username: 'joe@example.com'}),
     ]);
-    render(<Container organization={organization} forwardedValue="value" />, {
-      organization,
-    });
-
-    expect(screen.getByTestId('Suggested Values')).toHaveTextContent(
-      'me, my_teams, [me, my_teams, none], #best-team'
+    const {container} = render(
+      <Container organization={TestStubs.Organization()} forwardedValue="value" />
     );
 
-    expect(screen.getByTestId('All Values')).toHaveTextContent(
-      'foo@example.com, joe@example.com, #worst-team'
+    expect(container).toHaveTextContent(
+      'assigned: me, my_teams, [me, my_teams, none], #best-team'
     );
+    // Has the other teams/members
+    expect(container).toHaveTextContent('foo@example.com, joe@example.com, #worst-team');
   });
 });

+ 18 - 21
static/app/utils/withIssueTags.tsx

@@ -62,26 +62,23 @@ function withIssueTags<Props extends WithIssueTagsProps>(
 
       const meAndMyTeams = ['my_teams', '[me, my_teams, none]'];
       const suggestedAssignees: string[] = ['me', ...meAndMyTeams, ...userTeams];
-      const assigndValues: SearchGroup[] | string[] =
-        props.organization.features.includes('issue-search-shortcuts')
-          ? [
-              {
-                title: t('Suggested Values'),
-                type: 'header',
-                icon: <IconStar size="xs" />,
-                children: suggestedAssignees.map(convertToSearchItem),
-              },
-              {
-                title: t('All Values'),
-                type: 'header',
-                icon: <IconUser size="xs" />,
-                children: [
-                  ...usernames.map(convertToSearchItem),
-                  ...nonMemberTeams.map(convertToSearchItem),
-                ],
-              },
-            ]
-          : [...suggestedAssignees, ...usernames, ...nonMemberTeams];
+      const assigndValues: SearchGroup[] | string[] = [
+        {
+          title: t('Suggested Values'),
+          type: 'header',
+          icon: <IconStar size="xs" />,
+          children: suggestedAssignees.map(convertToSearchItem),
+        },
+        {
+          title: t('All Values'),
+          type: 'header',
+          icon: <IconUser size="xs" />,
+          children: [
+            ...usernames.map(convertToSearchItem),
+            ...nonMemberTeams.map(convertToSearchItem),
+          ],
+        },
+      ];
 
       return {
         ...tags,
@@ -98,7 +95,7 @@ function withIssueTags<Props extends WithIssueTagsProps>(
           values: assigndValues,
         },
       };
-    }, [props.organization, teams, members, tags]);
+    }, [teams, members, tags]);
 
     // Listen to tag store updates and cleanup listener on unmount
     useEffect(() => {

+ 0 - 83
static/app/views/issueList/filters.spec.tsx

@@ -1,83 +0,0 @@
-import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-
-import IssueListFilters from 'sentry/views/issueList/filters';
-
-describe('IssueListFilters', () => {
-  const onSearch = jest.fn();
-  const baseQuery = 'is:unresolved';
-
-  beforeEach(() => {
-    MockApiClient.addMockResponse({
-      method: 'GET',
-      url: '/organizations/org-slug/searches/',
-      body: [],
-    });
-  });
-
-  afterEach(() => {
-    jest.clearAllMocks();
-    MockApiClient.clearMockResponses();
-  });
-
-  it('should search the correct category when the IssueCategoryFilter dropdown is used', async () => {
-    render(<IssueListFilters query={baseQuery} onSearch={onSearch} />);
-
-    const filterDropdown = screen.getByRole('button', {name: 'All Categories'});
-    expect(filterDropdown).toBeInTheDocument();
-
-    await userEvent.click(filterDropdown);
-
-    const errorsOption = screen.getByTestId('error');
-    expect(errorsOption).toBeInTheDocument();
-    await userEvent.click(errorsOption);
-    expect(onSearch).toHaveBeenCalledWith(`${baseQuery} issue.category:error`);
-
-    await userEvent.click(filterDropdown);
-
-    const performanceOption = screen.getByTestId('performance');
-    expect(performanceOption).toBeInTheDocument();
-    await userEvent.click(performanceOption);
-    expect(onSearch).toHaveBeenCalledWith(`${baseQuery} issue.category:performance`);
-
-    await userEvent.click(filterDropdown);
-
-    const allCategoriesOption = screen.getByTestId('all_categories');
-    expect(allCategoriesOption).toBeInTheDocument();
-    await userEvent.click(allCategoriesOption);
-    expect(onSearch).toHaveBeenCalledWith(baseQuery);
-  });
-
-  it('should update the search bar query string when an IssueCategoryFilter dropdown option is selected', () => {
-    const {rerender} = render(<IssueListFilters query={baseQuery} onSearch={onSearch} />);
-
-    const filterDropdown = screen.getByTestId('issue-category-filter');
-    expect(filterDropdown).toHaveTextContent('All Categories');
-
-    rerender(
-      <IssueListFilters query={`${baseQuery} issue.category:error`} onSearch={onSearch} />
-    );
-    expect(filterDropdown).toHaveTextContent('Errors');
-
-    rerender(
-      <IssueListFilters
-        query={`${baseQuery} issue.category:performance`}
-        onSearch={onSearch}
-      />
-    );
-    expect(filterDropdown).toHaveTextContent('Performance');
-
-    rerender(<IssueListFilters query="" onSearch={onSearch} />);
-    expect(filterDropdown).toHaveTextContent('All Categories');
-  });
-
-  it('should filter by cron monitors', async () => {
-    render(<IssueListFilters query="" onSearch={onSearch} />, {
-      organization: TestStubs.Organization({features: ['issue-platform']}),
-    });
-
-    await userEvent.click(screen.getByRole('button', {name: 'All Categories'}));
-    await userEvent.click(screen.getByRole('option', {name: /Crons/}));
-
-    expect(onSearch).toHaveBeenCalledWith('issue.category:cron');
-  });
-});

+ 0 - 6
static/app/views/issueList/filters.tsx

@@ -5,8 +5,6 @@ import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import ProjectPageFilter from 'sentry/components/projectPageFilter';
 import {space} from 'sentry/styles/space';
-import useOrganization from 'sentry/utils/useOrganization';
-import IssueCategoryFilter from 'sentry/views/issueList/issueCategoryFilter';
 import {IssueSearchWithSavedSearches} from 'sentry/views/issueList/issueSearchWithSavedSearches';
 
 interface Props {
@@ -15,16 +13,12 @@ interface Props {
 }
 
 function IssueListFilters({query, onSearch}: Props) {
-  const organization = useOrganization();
   return (
     <SearchContainer>
       <StyledPageFilterBar>
         <ProjectPageFilter />
         <EnvironmentPageFilter />
         <DatePageFilter />
-        {organization.features.includes('issue-search-shortcuts') ? null : (
-          <IssueCategoryFilter query={query} onSearch={onSearch} />
-        )}
       </StyledPageFilterBar>
 
       <IssueSearchWithSavedSearches {...{query, onSearch}} />

+ 0 - 149
static/app/views/issueList/issueCategoryFilter.tsx

@@ -1,149 +0,0 @@
-import {useCallback, useEffect, useMemo, useState} from 'react';
-import styled from '@emotion/styled';
-
-import {CompactSelect, SelectOption} from 'sentry/components/compactSelect';
-import FeatureBadge from 'sentry/components/featureBadge';
-import {IconStack} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import {IssueCategory} from 'sentry/types';
-import {trackAnalytics} from 'sentry/utils/analytics';
-import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
-import useOrganization from 'sentry/utils/useOrganization';
-
-const ISSUE_CATEGORY_FILTER = 'issue.category';
-
-interface IssueCategoryFilterProps {
-  onSearch: (query: string) => void;
-  query: string;
-}
-
-function IssueCategoryFilter({query, onSearch}: IssueCategoryFilterProps) {
-  const [isPerformanceSeen, setIsPerformanceSeen] = useLocalStorageState(
-    'issue-category-dropdown-seen:performance',
-    false
-  );
-  const [isCronSeen, setIsCronSeen] = useLocalStorageState(
-    'issue-category-dropdown-seen:crons',
-    false
-  );
-  const organization = useOrganization();
-
-  const renderLabel = useCallback(
-    (issueCategory?: IssueCategory, isTriggerLabel?: boolean) => {
-      switch (issueCategory) {
-        case IssueCategory.ERROR:
-          return <LabelWrapper>{t('Errors')}</LabelWrapper>;
-        case IssueCategory.PERFORMANCE:
-          return (
-            <LabelWrapper>
-              {t('Performance')}
-              {!isTriggerLabel && !isPerformanceSeen && <FeatureBadge type="new" />}
-            </LabelWrapper>
-          );
-        case IssueCategory.CRON:
-          return (
-            <LabelWrapper>
-              {t('Crons')}
-              {!isTriggerLabel && !isCronSeen && <FeatureBadge type="new" />}
-            </LabelWrapper>
-          );
-        default:
-          return <LabelWrapper>{t('All Categories')}</LabelWrapper>;
-      }
-    },
-    [isPerformanceSeen, isCronSeen]
-  );
-
-  const options = useMemo(
-    () => [
-      {label: renderLabel(), value: 'all_categories', textValue: 'all_categories'},
-      {
-        label: renderLabel(IssueCategory.ERROR),
-        value: IssueCategory.ERROR,
-        textValue: IssueCategory.ERROR,
-      },
-      {
-        label: renderLabel(IssueCategory.PERFORMANCE),
-        value: IssueCategory.PERFORMANCE,
-        textValue: IssueCategory.PERFORMANCE,
-      },
-      ...(organization.features.includes('issue-platform')
-        ? [
-            {
-              label: renderLabel(IssueCategory.CRON),
-              value: IssueCategory.CRON,
-              textValue: IssueCategory.CRON,
-            },
-          ]
-        : []),
-    ],
-    [renderLabel, organization]
-  );
-
-  const [selectedOption, setSelectedOption] = useState<SelectOption<string>>(options[0]);
-
-  // Effect that handles setting the current option if the query is changed manually
-  useEffect(() => {
-    setSelectedOption(prevOption => {
-      const queryOption = options.find(({value}) =>
-        query.includes(`${ISSUE_CATEGORY_FILTER}:${value}`)
-      );
-
-      if (!queryOption) {
-        return options[0];
-      }
-
-      if (queryOption.value !== prevOption.value) {
-        return queryOption;
-      }
-
-      return prevOption;
-    });
-  }, [query, options]);
-
-  const handleChange = (option: SelectOption<string>) => {
-    const search = new MutableSearch(query);
-
-    if (option.value === 'all_categories') {
-      search.removeFilter(ISSUE_CATEGORY_FILTER);
-    } else {
-      search.setFilterValues(ISSUE_CATEGORY_FILTER, [option.value]);
-    }
-
-    if (option.value === IssueCategory.PERFORMANCE) {
-      setIsPerformanceSeen(true);
-    }
-    if (option.value === IssueCategory.CRON) {
-      setIsCronSeen(true);
-    }
-
-    trackAnalytics('issues_stream.issue_category_dropdown_changed', {
-      organization,
-      category: option.value,
-    });
-
-    setSelectedOption(option);
-    onSearch(search.formatString());
-  };
-
-  return (
-    <CompactSelect
-      data-test-id="issue-category-filter"
-      options={options}
-      value={selectedOption.value}
-      triggerProps={{icon: <IconStack />}}
-      triggerLabel={renderLabel(selectedOption.value as IssueCategory, true)}
-      onChange={handleChange}
-      menuWidth={250}
-      size="md"
-    />
-  );
-}
-
-const LabelWrapper = styled('div')`
-  overflow: hidden;
-  text-overflow: ellipsis;
-`;
-
-export default IssueCategoryFilter;

+ 1 - 2
static/app/views/issueList/searchBar.tsx

@@ -92,7 +92,6 @@ function IssueListSearchBar({organization, tags, ...props}: Props) {
     [tagValueLoader]
   );
 
-  const hasSearchShortcuts = organization.features.includes('issue-search-shortcuts');
   const recommendedGroup: SearchGroup = {
     title: t('Popular Filters'),
     type: 'header',
@@ -147,7 +146,7 @@ function IssueListSearchBar({organization, tags, ...props}: Props) {
       excludedTags={EXCLUDED_TAGS}
       maxMenuHeight={500}
       supportedTags={getSupportedTags(tags)}
-      defaultSearchGroup={hasSearchShortcuts ? recommendedGroup : undefined}
+      defaultSearchGroup={recommendedGroup}
       organization={organization}
       {...props}
     />