Browse Source

Implement separate thresholds route (#58202)

Implements a separate route for release thresholds and modifies the
header for the release index page.


https://github.com/getsentry/sentry/assets/6186377/6ef5f55a-ab3b-43d6-96e0-2c7143b2e120


All changes/routes are hidden behind an org feature flag.
if flag is not set, then release trheshold page will redirect to the
release page.



https://github.com/getsentry/sentry/assets/6186377/0cfc6d18-1908-4b76-becb-7efec23136fe
Nathan Hsieh 1 year ago
parent
commit
45a152bbe2

+ 5 - 0
src/sentry/web/urls.py

@@ -789,6 +789,11 @@ urlpatterns += [
         react_page_view,
         name="releases",
     ),
+    re_path(
+        r"^release-thresholds/",
+        react_page_view,
+        name="release-thresholds",
+    ),
     # User Feedback
     re_path(
         r"^user-feedback/",

+ 25 - 0
static/app/routes.tsx

@@ -1470,6 +1470,30 @@ function buildRoutes() {
       </Route>
     </Fragment>
   );
+  const releaseThresholdRoutes = (
+    <Fragment>
+      {usingCustomerDomain && (
+        <Route
+          path="/release-thresholds/"
+          component={withDomainRequired(NoOp)}
+          key="orgless-release-thresholds-route"
+        >
+          <IndexRoute
+            component={make(() => import('sentry/views/releases/thresholdsList'))}
+          />
+        </Route>
+      )}
+      <Route
+        path="/organizations/:orgId/release-thresholds/"
+        component={withDomainRedirect(NoOp)}
+        key="org-release-thresholds"
+      >
+        <IndexRoute
+          component={make(() => import('sentry/views/releases/thresholdsList'))}
+        />
+      </Route>
+    </Fragment>
+  );
 
   const activityRoutes = (
     <Fragment>
@@ -2291,6 +2315,7 @@ function buildRoutes() {
       {cronsRoutes}
       {replayRoutes}
       {releasesRoutes}
+      {releaseThresholdRoutes}
       {activityRoutes}
       {statsRoutes}
       {discoverRoutes}

+ 99 - 0
static/app/views/releases/components/header.tsx

@@ -0,0 +1,99 @@
+import {useState} from 'react';
+import {InjectedRouter} from 'react-router';
+import styled from '@emotion/styled';
+
+import * as Layout from 'sentry/components/layouts/thirds';
+import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
+import {TabList, Tabs} from 'sentry/components/tabs';
+import {Tooltip} from 'sentry/components/tooltip';
+import {SLOW_TOOLTIP_DELAY} from 'sentry/constants';
+import {t} from 'sentry/locale';
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
+
+import {MONITOR_PATH, THRESHOLDS_PATH} from '../utils/constants';
+
+type Props = {
+  hasV2ReleaseUIEnabled: boolean;
+  router: InjectedRouter;
+};
+
+function Header({router, hasV2ReleaseUIEnabled}: Props) {
+  const [selected, setSelected] = useState(router.location.pathname);
+
+  const location = router.location;
+  const {
+    cursor: _cursor,
+    page: _page,
+    view: _view,
+    ...queryParams
+  } = location?.query ?? {};
+
+  const tabs = hasV2ReleaseUIEnabled
+    ? [
+        {
+          key: MONITOR_PATH,
+          label: t('Monitor'),
+          description: '',
+          path: MONITOR_PATH,
+        },
+        {
+          key: THRESHOLDS_PATH,
+          label: t('Thresholds'),
+          description:
+            'thresholds represent action alerts that will trigger once a threshold has been breached',
+          path: THRESHOLDS_PATH,
+        },
+      ]
+    : [];
+
+  const onTabSelect = key => {
+    setSelected(key);
+  };
+
+  return (
+    <Layout.Header noActionWrap>
+      <Layout.HeaderContent>
+        <Layout.Title>
+          {t('Releases')}
+          <PageHeadingQuestionTooltip
+            docsUrl="https://docs.sentry.io/product/releases/"
+            title={t(
+              'A visualization of your release adoption from the past 24 hours, providing a high-level view of the adoption stage, percentage of crash-free users and sessions, and more.'
+            )}
+          />
+        </Layout.Title>
+      </Layout.HeaderContent>
+      <StyledTabs value={selected} onChange={onTabSelect}>
+        <TabList hideBorder>
+          {tabs.map(({key, label, description, path}) => {
+            const to_url = normalizeUrl({
+              query: {
+                ...queryParams,
+              },
+              pathname: path,
+            });
+
+            return (
+              <TabList.Item key={key} to={to_url} textValue={label}>
+                <Tooltip
+                  title={description}
+                  position="bottom"
+                  isHoverable
+                  delay={SLOW_TOOLTIP_DELAY}
+                >
+                  {label}
+                </Tooltip>
+              </TabList.Item>
+            );
+          })}
+        </TabList>
+      </StyledTabs>
+    </Layout.Header>
+  );
+}
+
+export default Header;
+
+const StyledTabs = styled(Tabs)`
+  grid-column: 1/-1;
+`;

+ 4 - 15
static/app/views/releases/list/index.tsx

@@ -17,7 +17,6 @@ import NoProjectMessage from 'sentry/components/noProjectMessage';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
 import {getRelativeSummary} from 'sentry/components/organizations/timeRangeSelector/utils';
-import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
 import Pagination from 'sentry/components/pagination';
 import Panel from 'sentry/components/panels/panel';
 import ProjectPageFilter from 'sentry/components/projectPageFilter';
@@ -47,6 +46,7 @@ import withPageFilters from 'sentry/utils/withPageFilters';
 import withProjects from 'sentry/utils/withProjects';
 import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
 
+import Header from '../components/header';
 import ReleaseArchivedNotice from '../detail/overview/releaseArchivedNotice';
 import {isMobileRelease} from '../utils';
 
@@ -75,6 +75,7 @@ type State = {
 class ReleasesList extends DeprecatedAsyncView<Props, State> {
   shouldReload = true;
   shouldRenderBadRequests = true;
+  hasV2ReleaseUIEnabled = this.props.organization.features.includes('release-ui-v2');
 
   getTitle() {
     return routeTitleGen(t('Releases'), this.props.organization.slug, false);
@@ -502,7 +503,7 @@ class ReleasesList extends DeprecatedAsyncView<Props, State> {
   }
 
   renderBody() {
-    const {organization, selection} = this.props;
+    const {organization, selection, router} = this.props;
     const {releases, reloading, error} = this.state;
 
     const activeSort = this.getSort();
@@ -520,19 +521,7 @@ class ReleasesList extends DeprecatedAsyncView<Props, State> {
     return (
       <PageFiltersContainer showAbsolute={false}>
         <NoProjectMessage organization={organization}>
-          <Layout.Header>
-            <Layout.HeaderContent>
-              <Layout.Title>
-                {t('Releases')}
-                <PageHeadingQuestionTooltip
-                  docsUrl="https://docs.sentry.io/product/releases/"
-                  title={t(
-                    'A visualization of your release adoption from the past 24 hours, providing a high-level view of the adoption stage, percentage of crash-free users and sessions, and more.'
-                  )}
-                />
-              </Layout.Title>
-            </Layout.HeaderContent>
-          </Layout.Header>
+          <Header router={router} hasV2ReleaseUIEnabled={this.hasV2ReleaseUIEnabled} />
 
           <Layout.Body>
             <Layout.Main fullWidth>

+ 36 - 0
static/app/views/releases/thresholdsList/index.tsx

@@ -0,0 +1,36 @@
+import {useEffect} from 'react';
+import {RouteComponentProps} from 'react-router';
+
+// import {Organization, PageFilters, Project} from 'sentry/types';
+import useOrganization from 'sentry/utils/useOrganization';
+
+// import usePageFilters from 'sentry/utils/usePageFilters';
+// import useProjects from 'sentry/utils/useProjects';
+// import useRouter from 'sentry/utils/useRouter';
+import Header from '../components/header';
+
+type RouteParams = {
+  orgId: string;
+};
+
+type Props = RouteComponentProps<RouteParams, {}> & {};
+
+function ReleaseThresholdList({router}: Props) {
+  const organization = useOrganization();
+  useEffect(() => {
+    const hasV2ReleaseUIEnabled = organization.features.includes('release-ui-v2');
+    if (!hasV2ReleaseUIEnabled) {
+      router.replace('/releases/');
+    }
+  }, [router, organization]);
+  // const {projects, initiallyLoaded: projectsLoaded} = useProjects();
+  // const {selection, isReady, desyncedFilters} = usePageFilters();
+
+  return (
+    <div>
+      <Header router={router} hasV2ReleaseUIEnabled />
+    </div>
+  );
+}
+
+export default ReleaseThresholdList;

+ 2 - 0
static/app/views/releases/utils/constants.ts

@@ -0,0 +1,2 @@
+export const THRESHOLDS_PATH = '/release-thresholds/';
+export const MONITOR_PATH = '/releases/';

+ 4 - 1
static/app/views/releases/utils/useFetchThresholdsListData.tsx

@@ -31,6 +31,9 @@ export default function useFetchThresholdsListData({
         query,
       },
     ],
-    {staleTime: 0}
+    {
+      staleTime: 0,
+      enabled: organization.features?.includes('event-attachments') ?? false,
+    }
   );
 }