layout.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import {Fragment, memo, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import emptyStateImg from 'sentry-images/spot/custom-metrics-empty-state.svg';
  4. import {Button} from 'sentry/components/button';
  5. import ButtonBar from 'sentry/components/buttonBar';
  6. import FeatureBadge from 'sentry/components/featureBadge';
  7. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  8. import {GithubFeedbackButton} from 'sentry/components/githubFeedbackButton';
  9. import FullViewport from 'sentry/components/layouts/fullViewport';
  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 SplitPanel, {BaseSplitDivider, DividerProps} from 'sentry/components/splitPanel';
  19. import {IconGrabbable} from 'sentry/icons';
  20. import {t} from 'sentry/locale';
  21. import {space} from 'sentry/styles/space';
  22. import {useDimensions} from 'sentry/utils/useDimensions';
  23. import {useDDMContext} from 'sentry/views/ddm/context';
  24. import {useMetricsOnboardingSidebar} from 'sentry/views/ddm/ddmOnboarding/useMetricsOnboardingSidebar';
  25. import {MetricScratchpad} from 'sentry/views/ddm/scratchpad';
  26. import {ScratchpadSelector} from 'sentry/views/ddm/scratchpadSelector';
  27. import {TrayContent} from 'sentry/views/ddm/trayContent';
  28. const SIZE_LOCAL_STORAGE_KEY = 'ddm-split-size';
  29. function MainContent() {
  30. const {metricsMeta, hasCustomMetrics, isLoading} = useDDMContext();
  31. const hasMetrics = !isLoading && metricsMeta.length > 0;
  32. const {activateSidebar} = useMetricsOnboardingSidebar();
  33. return (
  34. <Fragment>
  35. <Layout.Header>
  36. <Layout.HeaderContent>
  37. <Layout.Title>
  38. {t('Metrics')}
  39. <PageHeadingQuestionTooltip
  40. docsUrl="https://develop.sentry.dev/delightful-developer-metrics/"
  41. title={t('Delightful Developer Metrics.')}
  42. />
  43. <FeatureBadge type="alpha" />
  44. </Layout.Title>
  45. </Layout.HeaderContent>
  46. <Layout.HeaderActions>
  47. <ButtonBar gap={1}>
  48. {hasMetrics && !hasCustomMetrics && (
  49. <Button priority="primary" onClick={activateSidebar} size="sm">
  50. {t('Add Custom Metric')}
  51. </Button>
  52. )}
  53. <FeedbackWidgetButton />
  54. <GithubFeedbackButton
  55. href="https://github.com/getsentry/sentry/discussions/58584"
  56. label={t('Discussion')}
  57. title={null}
  58. />
  59. </ButtonBar>
  60. </Layout.HeaderActions>
  61. </Layout.Header>
  62. <Layout.Body>
  63. <Layout.Main fullWidth>
  64. <PaddedContainer>
  65. <PageFilterBar condensed>
  66. <ProjectPageFilter />
  67. <EnvironmentPageFilter />
  68. <DatePageFilter />
  69. </PageFilterBar>
  70. <ScratchpadSelector />
  71. </PaddedContainer>
  72. {isLoading ? (
  73. <LoadingIndicator />
  74. ) : hasMetrics ? (
  75. <MetricScratchpad />
  76. ) : (
  77. <OnboardingPanel image={<EmptyStateImage src={emptyStateImg} />}>
  78. <h3>{t('Get started with custom metrics')}</h3>
  79. <p>
  80. {t(
  81. "Send your own metrics to Sentry to track your system's behaviour and profit from the same powerful features as you do with errors, like alerting and dashboards."
  82. )}
  83. </p>
  84. <Button priority="primary" onClick={activateSidebar}>
  85. {t('Add Custom Metric')}
  86. </Button>
  87. </OnboardingPanel>
  88. )}
  89. </Layout.Main>
  90. </Layout.Body>
  91. </Fragment>
  92. );
  93. }
  94. export const DDMLayout = memo(() => {
  95. const measureRef = useRef<HTMLDivElement>(null);
  96. const {height} = useDimensions({elementRef: measureRef});
  97. const hasSize = height > 0;
  98. return (
  99. <FullViewport ref={measureRef}>
  100. {
  101. // FullViewport has a grid layout with `grid-template-rows: auto 1fr;`
  102. // therefore we need the empty div so that SplitPanel can span the whole height
  103. // TODO(arthur): Check on the styles of FullViewport
  104. }
  105. <div />
  106. {hasSize && (
  107. <SplitPanel
  108. availableSize={height}
  109. SplitDivider={SplitDivider}
  110. sizeStorageKey={SIZE_LOCAL_STORAGE_KEY}
  111. top={{
  112. content: (
  113. <ScrollingPage>
  114. <MainContent />
  115. </ScrollingPage>
  116. ),
  117. default: height * 0.7,
  118. min: 100,
  119. max: height - 58,
  120. }}
  121. bottom={<TrayContent />}
  122. />
  123. )}
  124. </FullViewport>
  125. );
  126. });
  127. const SplitDivider = styled((props: DividerProps) => (
  128. <BaseSplitDivider {...props} icon={<IconGrabbable size="xs" />} />
  129. ))<DividerProps>`
  130. border-top: 1px solid ${$p => $p.theme.border};
  131. `;
  132. const ScrollingPage = styled(Layout.Page)`
  133. height: 100%;
  134. overflow: auto;
  135. `;
  136. const PaddedContainer = styled('div')`
  137. margin-bottom: ${space(2)};
  138. display: flex;
  139. justify-content: space-between;
  140. flex-wrap: wrap;
  141. gap: ${space(1)};
  142. `;
  143. const EmptyStateImage = styled('img')`
  144. @media (min-width: ${p => p.theme.breakpoints.small}) {
  145. user-select: none;
  146. position: absolute;
  147. top: 0;
  148. bottom: 0;
  149. width: 220px;
  150. margin-top: auto;
  151. margin-bottom: auto;
  152. transform: translateX(-50%);
  153. left: 50%;
  154. }
  155. @media (min-width: ${p => p.theme.breakpoints.large}) {
  156. transform: translateX(-60%);
  157. width: 280px;
  158. }
  159. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  160. transform: translateX(-75%);
  161. width: 320px;
  162. }
  163. `;