layout.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import {Fragment, memo, useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import emptyStateImg from 'sentry-images/spot/custom-metrics-empty-state.svg';
  5. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  6. import FeatureBadge from 'sentry/components/badge/featureBadge';
  7. import {Button, LinkButton} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import FloatingFeedbackWidget from 'sentry/components/feedback/widget/floatingFeedbackWidget';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import OnboardingPanel from 'sentry/components/onboardingPanel';
  13. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  14. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  15. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  16. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  17. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  18. import {t} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import type {Organization} from 'sentry/types/organization';
  21. import {trackAnalytics} from 'sentry/utils/analytics';
  22. import {METRICS_DOCS_URL} from 'sentry/utils/metrics/constants';
  23. import {useVirtualMetricsContext} from 'sentry/utils/metrics/virtualMetricsContext';
  24. import useDismissAlert from 'sentry/utils/useDismissAlert';
  25. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  26. import useOrganization from 'sentry/utils/useOrganization';
  27. import usePageFilters from 'sentry/utils/usePageFilters';
  28. import {useMetricsContext} from 'sentry/views/metrics/context';
  29. import {useMetricsOnboardingSidebar} from 'sentry/views/metrics/ddmOnboarding/useMetricsOnboardingSidebar';
  30. import {IntervalSelect} from 'sentry/views/metrics/intervalSelect';
  31. import {MetricsBetaEndAlert} from 'sentry/views/metrics/metricsBetaEndAlert';
  32. import {PageHeaderActions} from 'sentry/views/metrics/pageHeaderActions';
  33. import {Queries} from 'sentry/views/metrics/queries';
  34. import {MetricScratchpad} from 'sentry/views/metrics/scratchpad';
  35. import {WidgetDetails} from 'sentry/views/metrics/widgetDetails';
  36. function showEmptyState({
  37. isEmptyStateDismissed,
  38. hasSentCustomMetrics,
  39. }: {
  40. hasPerformanceMetrics: boolean;
  41. hasSentCustomMetrics: boolean;
  42. isEmptyStateDismissed: boolean;
  43. organization: Organization;
  44. }) {
  45. return !isEmptyStateDismissed && !hasSentCustomMetrics;
  46. }
  47. export const MetricsLayout = memo(() => {
  48. const organization = useOrganization();
  49. const pageFilters = usePageFilters();
  50. const selectedProjects = pageFilters.selection.projects.join();
  51. const {
  52. hasCustomMetrics: hasSentCustomMetrics,
  53. hasPerformanceMetrics,
  54. isHasMetricsLoading,
  55. } = useMetricsContext();
  56. const virtualMetrics = useVirtualMetricsContext();
  57. const isLoading = isHasMetricsLoading || virtualMetrics.isLoading;
  58. const {activateSidebar} = useMetricsOnboardingSidebar();
  59. const {dismiss: emptyStateDismiss, isDismissed: isEmptyStateDismissed} =
  60. useDismissAlert({
  61. key: `${organization.id}:${selectedProjects}:metrics-empty-state-dismissed`,
  62. });
  63. const [isBannerDismissed] = useLocalStorageState('metrics-banner-dismissed', false);
  64. const addCustomMetric = useCallback(
  65. (referrer: 'header' | 'onboarding_panel' | 'banner') => {
  66. Sentry.metrics.increment('ddm.add_custom_metric', 1, {
  67. tags: {
  68. referrer,
  69. },
  70. });
  71. trackAnalytics('ddm.open-onboarding', {
  72. organization,
  73. source: referrer,
  74. });
  75. activateSidebar();
  76. },
  77. [activateSidebar, organization]
  78. );
  79. const viewPerformanceMetrics = useCallback(() => {
  80. Sentry.metrics.increment('ddm.view_performance_metrics', 1);
  81. trackAnalytics('ddm.view_performance_metrics', {
  82. organization,
  83. });
  84. emptyStateDismiss();
  85. }, [emptyStateDismiss, organization]);
  86. const showOnboardingPanel = showEmptyState({
  87. organization,
  88. isEmptyStateDismissed,
  89. hasPerformanceMetrics,
  90. hasSentCustomMetrics,
  91. });
  92. return (
  93. <Fragment>
  94. <Layout.Header>
  95. <Layout.HeaderContent>
  96. <Layout.Title>
  97. {t('Metrics')}
  98. <PageHeadingQuestionTooltip
  99. docsUrl={METRICS_DOCS_URL}
  100. title={t(
  101. 'Metrics help you track and visualize the data points you care about, making it easier to monitor your application health and identify issues.'
  102. )}
  103. />
  104. <FeatureBadge
  105. type="beta"
  106. title={t(
  107. 'The Metrics beta will end and we will retire the current solution on October 7th, 2024'
  108. )}
  109. />
  110. </Layout.Title>
  111. </Layout.HeaderContent>
  112. <Layout.HeaderActions>
  113. {!showOnboardingPanel ? (
  114. <PageHeaderActions
  115. showAddMetricButton={
  116. hasSentCustomMetrics || (isEmptyStateDismissed && isBannerDismissed)
  117. }
  118. addCustomMetric={() => addCustomMetric('header')}
  119. />
  120. ) : null}
  121. </Layout.HeaderActions>
  122. </Layout.Header>
  123. <Layout.Body>
  124. <FloatingFeedbackWidget />
  125. <Layout.Main fullWidth>
  126. <MetricsBetaEndAlert organization={organization} />
  127. <FilterContainer>
  128. <PageFilterBar condensed>
  129. <ProjectPageFilter />
  130. <EnvironmentPageFilter />
  131. <DatePageFilter />
  132. </PageFilterBar>
  133. <IntervalSelect />
  134. </FilterContainer>
  135. {isLoading ? (
  136. <LoadingIndicator />
  137. ) : !showOnboardingPanel ? (
  138. <Fragment>
  139. <GuideAnchor target="metrics_onboarding" />
  140. <Queries />
  141. <MetricScratchpad />
  142. <WidgetDetails />
  143. </Fragment>
  144. ) : (
  145. <OnboardingPanel image={<EmptyStateImage src={emptyStateImg} />}>
  146. <h3>{t('Track and solve what matters')}</h3>
  147. <Fragment>
  148. <p>
  149. {t(
  150. 'Create custom metrics to track and visualize the data points you care about over time, like processing time, checkout conversion rate, or user signups. See correlated trace exemplars and metrics if used together with Performance Monitoring.'
  151. )}
  152. </p>
  153. <ButtonList gap={1}>
  154. <Button
  155. priority="primary"
  156. onClick={() => addCustomMetric('onboarding_panel')}
  157. >
  158. {t('Set Up Custom Metric')}
  159. </Button>
  160. <LinkButton href={METRICS_DOCS_URL} external>
  161. {t('Read Docs')}
  162. </LinkButton>
  163. {hasPerformanceMetrics && (
  164. <Button onClick={viewPerformanceMetrics}>
  165. {t('View Performance Metrics')}
  166. </Button>
  167. )}
  168. </ButtonList>
  169. </Fragment>
  170. </OnboardingPanel>
  171. )}
  172. </Layout.Main>
  173. </Layout.Body>
  174. </Fragment>
  175. );
  176. });
  177. const FilterContainer = styled('div')`
  178. margin-bottom: ${space(2)};
  179. display: flex;
  180. justify-content: flex-start;
  181. flex-wrap: wrap;
  182. gap: ${space(1)};
  183. `;
  184. const EmptyStateImage = styled('img')`
  185. @media (min-width: ${p => p.theme.breakpoints.small}) {
  186. user-select: none;
  187. position: absolute;
  188. top: 0;
  189. bottom: 0;
  190. width: 220px;
  191. margin-top: auto;
  192. margin-bottom: auto;
  193. transform: translateX(-50%);
  194. left: 50%;
  195. }
  196. @media (min-width: ${p => p.theme.breakpoints.large}) {
  197. transform: translateX(-60%);
  198. width: 280px;
  199. }
  200. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  201. transform: translateX(-75%);
  202. width: 320px;
  203. }
  204. `;
  205. const ButtonList = styled(ButtonBar)`
  206. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  207. grid-auto-flow: row;
  208. gap: ${space(1)};
  209. `;