Browse Source

feat(starfish): Make release selector searchable (#58190)

Also adds session counts to the selector.
![Screenshot 2023-10-16 at 4 00
31 PM](https://github.com/getsentry/sentry/assets/63818634/00c2d807-b066-4ac3-99b1-525073502960)
Shruthi 1 year ago
parent
commit
80e3c10277

+ 67 - 15
static/app/views/starfish/components/releaseSelector.tsx

@@ -1,21 +1,21 @@
+import {useState} from 'react';
 import {browserHistory} from 'react-router';
 import styled from '@emotion/styled';
+import debounce from 'lodash/debounce';
 
-import {CompactSelect} from 'sentry/components/compactSelect';
+import {CompactSelect, SelectOption} from 'sentry/components/compactSelect';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
-import {t} from 'sentry/locale';
+import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
+import {t, tn} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
 import {defined} from 'sentry/utils';
 import {useLocation} from 'sentry/utils/useLocation';
 import {
   useReleases,
   useReleaseSelection,
+  useReleaseStats,
 } from 'sentry/views/starfish/queries/useReleases';
 
-const ALL_RELEASES = {
-  value: '',
-  label: t('All Releases'),
-};
-
 type Props = {
   selectorKey: string;
   selectorName?: string;
@@ -23,25 +23,51 @@ type Props = {
 };
 
 export function ReleaseSelector({selectorName, selectorKey, selectorValue}: Props) {
-  const {data, isLoading} = useReleases();
+  const [searchTerm, setSearchTerm] = useState<string | undefined>(undefined);
+  const {data, isLoading} = useReleases(searchTerm);
+  const {data: releaseStats} = useReleaseStats();
   const location = useLocation();
-  let value = selectorValue;
 
-  if (!isLoading && !defined(value)) {
-    value = ALL_RELEASES.value;
+  const options: SelectOption<string>[] = [];
+  if (defined(selectorValue)) {
+    options.push({
+      value: selectorValue,
+      label: selectorValue,
+    });
   }
+  data
+    ?.filter(({version}) => selectorValue !== version)
+    .forEach(release => {
+      const option = {
+        value: release.version,
+        label: release.version,
+        details: (
+          <LabelDetails sessionCount={releaseStats[release.version]?.['sum(session)']} />
+        ),
+      };
+
+      options.push(option);
+    });
+
   return (
     <StyledCompactSelect
       triggerProps={{
         prefix: selectorName,
+        title: selectorValue,
       }}
+      loading={isLoading}
+      searchable
       value={selectorValue}
       options={[
-        ...(data ?? [ALL_RELEASES]).map(release => ({
-          value: release.version,
-          label: release.shortVersion ?? release.version,
-        })),
+        {
+          value: '_releases',
+          label: t('Sorted by date created'),
+          options,
+        },
       ]}
+      onSearch={debounce(val => {
+        setSearchTerm(val);
+      }, DEFAULT_DEBOUNCE_DURATION)}
       onChange={newValue => {
         browserHistory.push({
           ...location,
@@ -51,10 +77,29 @@ export function ReleaseSelector({selectorName, selectorKey, selectorValue}: Prop
           },
         });
       }}
+      onClose={() => {
+        setSearchTerm(undefined);
+      }}
     />
   );
 }
 
+type LabelDetailsProps = {
+  sessionCount?: number;
+};
+
+function LabelDetails(props: LabelDetailsProps) {
+  return (
+    <DetailsContainer>
+      <div>
+        {defined(props.sessionCount)
+          ? tn('%s session', '%s sessions', props.sessionCount)
+          : '-'}
+      </div>
+    </DetailsContainer>
+  );
+}
+
 export function ReleaseComparisonSelector() {
   const {primaryRelease, secondaryRelease} = useReleaseSelection();
   return (
@@ -74,3 +119,10 @@ const StyledCompactSelect = styled(CompactSelect)`
     max-width: 275px;
   }
 `;
+
+const DetailsContainer = styled('div')`
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  gap: ${space(1)};
+`;

+ 61 - 1
static/app/views/starfish/queries/useReleases.tsx

@@ -1,11 +1,14 @@
+import {getInterval} from 'sentry/components/charts/utils';
+import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
 import {Release} from 'sentry/types';
+import {defined} from 'sentry/utils';
 import {useApiQuery} from 'sentry/utils/queryClient';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
 
-export function useReleases() {
+export function useReleases(searchTerm?: string) {
   const organization = useOrganization();
   const {selection} = usePageFilters();
   const {environments, projects} = selection;
@@ -19,6 +22,7 @@ export function useReleases() {
           project: projects,
           per_page: 50,
           environment: environments,
+          query: searchTerm,
         },
       },
     ],
@@ -39,3 +43,59 @@ export function useReleaseSelection() {
 
   return {primaryRelease, secondaryRelease, isLoading};
 }
+
+export function useReleaseStats() {
+  const organization = useOrganization();
+  const {selection} = usePageFilters();
+  const {environments, projects} = selection;
+
+  const {start, end, statsPeriod} = normalizeDateTimeParams(selection.datetime, {
+    allowEmptyPeriod: true,
+  });
+
+  // The sessions endpoint does not support wildcard search.
+  // So we're just getting top 250 values ordered by count.
+  // Hopefully this is enough to populate session count for
+  // any releases searched in the release selector.
+  const urlQuery = Object.fromEntries(
+    Object.entries({
+      project: projects,
+      environment: environments,
+      field: ['sum(session)'],
+      groupBy: ['release', 'project'],
+      orderBy: '-sum(session)',
+      start,
+      end,
+      statsPeriod,
+      per_page: 250,
+      interval: getInterval({start, end, period: statsPeriod}, 'low'),
+    }).filter(([, value]) => defined(value) && value !== '')
+  );
+
+  const result = useApiQuery<any>(
+    [
+      `/organizations/${organization.slug}/sessions/`,
+      {
+        query: urlQuery,
+      },
+    ],
+    {staleTime: Infinity}
+  );
+
+  const releaseStatsMap: {[release: string]: {project: number; 'sum(session)': number}} =
+    {};
+  if (result.data && result.data.groups) {
+    result.data.groups.forEach(group => {
+      const release = group.by.release;
+      const project = group.by.project;
+      const sessionCount = group.totals['sum(session)'];
+
+      releaseStatsMap[release] = {project, 'sum(session)': sessionCount};
+    });
+  }
+
+  return {
+    ...result,
+    data: releaseStatsMap,
+  };
+}