Priscila Oliveira 4 лет назад
Родитель
Сommit
45b746f7d9

+ 20 - 0
src/sentry/static/sentry/app/components/sidebar/index.tsx

@@ -15,6 +15,7 @@ import {extractSelectionParameters} from 'app/components/organizations/globalSel
 import {
   IconActivity,
   IconChevron,
+  IconDashboard,
   IconGraph,
   IconIssues,
   IconLab,
@@ -350,6 +351,24 @@ class Sidebar extends React.Component<Props, State> {
       </Feature>
     );
 
+    const metrics = hasOrganization && (
+      <Feature features={['metrics']} organization={organization}>
+        <SidebarItem
+          {...sidebarItemProps}
+          onClick={(_id, evt) =>
+            this.navigateWithGlobalSelection(
+              `/organizations/${organization.slug}/metrics/`,
+              evt
+            )
+          }
+          icon={<IconDashboard size="md" />}
+          label={t('Metrics')}
+          to={`/organizations/${organization.slug}/metrics/`}
+          id="metrics"
+        />
+      </Feature>
+    );
+
     const dashboards = hasOrganization && (
       <Feature
         features={['discover', 'discover-query', 'dashboards-basic', 'dashboards-edit']}
@@ -433,6 +452,7 @@ class Sidebar extends React.Component<Props, State> {
                 </SidebarSection>
 
                 <SidebarSection>
+                  {metrics}
                   {dashboards}
                   {monitors}
                 </SidebarSection>

+ 37 - 0
src/sentry/static/sentry/app/routes.tsx

@@ -19,6 +19,7 @@ import App from 'app/views/app';
 import AuthLayout from 'app/views/auth/layout';
 import IssueListContainer from 'app/views/issueList/container';
 import IssueListOverview from 'app/views/issueList/overview';
+import {MetricsTab} from 'app/views/metrics/utils';
 import OrganizationContext from 'app/views/organizationContext';
 import OrganizationDetails, {
   LightWeightOrganizationDetails,
@@ -1186,6 +1187,42 @@ function routes() {
             />
           </Route>
 
+          <Redirect
+            from="/organizations/:orgId/metrics/"
+            to="/organizations/:orgId/metrics/explorer/"
+          />
+
+          <Route
+            path="/organizations/:orgId/metrics/"
+            componentPromise={() =>
+              import(/* webpackChunkName: "Metrics" */ 'app/views/metrics')
+            }
+            component={errorHandler(LazyLoad)}
+          >
+            <Route path="explorer/">
+              <IndexRoute
+                component={errorHandler(LazyLoad)}
+                componentPromise={() =>
+                  import(
+                    /* webpackChunkName: "MetricsExplorer" */ 'app/views/metrics/explorer'
+                  )
+                }
+                props={{currentTab: MetricsTab.EXPLORER}}
+              />
+            </Route>
+            <Route path="dashboards/">
+              <IndexRoute
+                component={errorHandler(LazyLoad)}
+                componentPromise={() =>
+                  import(
+                    /* webpackChunkName: "MetricsDashboards" */ 'app/views/metrics/dashboards'
+                  )
+                }
+                props={{currentTab: MetricsTab.DASHBOARDS}}
+              />
+            </Route>
+          </Route>
+
           <Route
             path="/organizations/:orgId/user-feedback/"
             componentPromise={() =>

+ 30 - 0
src/sentry/static/sentry/app/views/metrics/dashboards.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+
+import {t} from 'app/locale';
+import {Organization} from 'app/types';
+import routeTitle from 'app/utils/routeTitle';
+import withOrganization from 'app/utils/withOrganization';
+import AsyncView from 'app/views/asyncView';
+
+type Props = AsyncView['props'] & {
+  organization: Organization;
+};
+
+type State = AsyncView['state'] & {};
+
+class Dashboards extends AsyncView<Props, State> {
+  getTitle() {
+    const {organization} = this.props;
+    return routeTitle(t('Metrics - Dashboards'), organization.slug, false);
+  }
+
+  renderLoading() {
+    return this.renderBody();
+  }
+
+  renderBody() {
+    return <div>Dashboards</div>;
+  }
+}
+
+export default withOrganization(Dashboards);

+ 30 - 0
src/sentry/static/sentry/app/views/metrics/explorer.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+
+import {t} from 'app/locale';
+import {Organization} from 'app/types';
+import routeTitle from 'app/utils/routeTitle';
+import withOrganization from 'app/utils/withOrganization';
+import AsyncView from 'app/views/asyncView';
+
+type Props = AsyncView['props'] & {
+  organization: Organization;
+};
+
+type State = AsyncView['state'] & {};
+
+class Explorer extends AsyncView<Props, State> {
+  getTitle() {
+    const {organization} = this.props;
+    return routeTitle(t('Metrics - Explorer'), organization.slug, false);
+  }
+
+  renderLoading() {
+    return this.renderBody();
+  }
+
+  renderBody() {
+    return <div>Explorer</div>;
+  }
+}
+
+export default withOrganization(Explorer);

+ 34 - 0
src/sentry/static/sentry/app/views/metrics/index.tsx

@@ -0,0 +1,34 @@
+import React from 'react';
+import {RouteComponentProps} from 'react-router';
+
+import Feature from 'app/components/acl/feature';
+import Alert from 'app/components/alert';
+import {t} from 'app/locale';
+import {PageContent} from 'app/styles/organization';
+import {Organization} from 'app/types';
+import withOrganization from 'app/utils/withOrganization';
+
+import Metrics from './metrics';
+
+type Props = RouteComponentProps<{}, {}> & {
+  organization: Organization;
+  children: React.ReactNode;
+};
+
+function MetricsContainer({organization, ...props}: Props) {
+  return (
+    <Feature
+      features={['metrics']}
+      organization={organization}
+      renderDisabled={() => (
+        <PageContent>
+          <Alert type="warning">{t("You don't have access to this feature")}</Alert>
+        </PageContent>
+      )}
+    >
+      <Metrics {...props} />
+    </Feature>
+  );
+}
+
+export default withOrganization(MetricsContainer);

+ 100 - 0
src/sentry/static/sentry/app/views/metrics/metrics.tsx

@@ -0,0 +1,100 @@
+import React from 'react';
+import {RouteComponentProps} from 'react-router';
+import styled from '@emotion/styled';
+
+import Button from 'app/components/button';
+import ButtonBar from 'app/components/buttonBar';
+import * as Layout from 'app/components/layouts/thirds';
+import Link from 'app/components/links/link';
+import ListItem from 'app/components/list/listItem';
+import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
+import {t} from 'app/locale';
+import {PageContent} from 'app/styles/organization';
+import space from 'app/styles/space';
+import {Organization} from 'app/types';
+import withOrganization from 'app/utils/withOrganization';
+
+import {getCurrentMetricsTab, MetricsTab} from './utils';
+
+type Props = RouteComponentProps<{}, {}> & {
+  organization: Organization;
+  children: React.ReactNode;
+};
+
+function Metrics({organization, children, routes}: Props) {
+  const {currentTab} = getCurrentMetricsTab(routes);
+  const orgSlug = organization.slug;
+
+  return (
+    <GlobalSelectionHeader skipLoadLastUsed>
+      <StyledPageContent>
+        <BorderlessHeader>
+          <StyledLayoutHeaderContent>
+            <StyledLayoutTitle>{t('Metrics')}</StyledLayoutTitle>
+          </StyledLayoutHeaderContent>
+          <Layout.HeaderActions>
+            <ButtonBar gap={1}>
+              <Button
+                title={t(
+                  "You’re seeing the metrics project because you have the feature flag 'organizations:metrics' enabled. Send us feedback via email."
+                )}
+                href="mailto:metrics-feedback@sentry.io?subject=Metrics Feedback"
+              >
+                {t('Give Feedback')}
+              </Button>
+            </ButtonBar>
+          </Layout.HeaderActions>
+        </BorderlessHeader>
+        <TabLayoutHeader>
+          <Layout.HeaderNavTabs underlined>
+            <ListItem className={currentTab === MetricsTab.EXPLORER ? 'active' : ''}>
+              <Link to={`/organizations/${orgSlug}/metrics/explorer/`}>
+                {t('Explorer')}
+              </Link>
+            </ListItem>
+            <ListItem className={currentTab === MetricsTab.DASHBOARDS ? 'active' : ''}>
+              <Link to={`/organizations/${orgSlug}/metrics/dashboards/`}>
+                {t('Dashboards')}
+              </Link>
+            </ListItem>
+          </Layout.HeaderNavTabs>
+        </TabLayoutHeader>
+        <Layout.Body>
+          <Layout.Main fullWidth>{children}</Layout.Main>
+        </Layout.Body>
+      </StyledPageContent>
+    </GlobalSelectionHeader>
+  );
+}
+
+export default withOrganization(Metrics);
+
+const StyledPageContent = styled(PageContent)`
+  padding: 0;
+`;
+
+const BorderlessHeader = styled(Layout.Header)`
+  border-bottom: 0;
+
+  /* Not enough buttons to change direction for mobile view */
+  @media (max-width: ${p => p.theme.breakpoints[1]}) {
+    flex-direction: row;
+  }
+`;
+
+const TabLayoutHeader = styled(Layout.Header)`
+  padding-top: 0;
+
+  @media (max-width: ${p => p.theme.breakpoints[1]}) {
+    padding-top: 0;
+  }
+`;
+
+const StyledLayoutHeaderContent = styled(Layout.HeaderContent)`
+  margin-bottom: 0;
+  margin-right: ${space(2)};
+`;
+
+const StyledLayoutTitle = styled(Layout.Title)`
+  margin-top: ${space(0.5)};
+`;

+ 17 - 0
src/sentry/static/sentry/app/views/metrics/utils.tsx

@@ -0,0 +1,17 @@
+import {PlainRoute} from 'react-router/lib/Route';
+
+export enum MetricsTab {
+  EXPLORER = 'explorer',
+  DASHBOARDS = 'dashboards',
+}
+
+export function getCurrentMetricsTab(
+  routes: Array<PlainRoute>
+): {currentTab: MetricsTab} {
+  // All the routes under /organizations/:orgId/metrics/ have a defined props
+  const {currentTab} = routes[routes.length - 1].props as {
+    currentTab: MetricsTab;
+  };
+
+  return {currentTab};
+}