chartPanel.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import {useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {openInsightChartModal} from 'sentry/actionCreators/modal';
  4. import {Button} from 'sentry/components/button';
  5. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  6. import Panel from 'sentry/components/panels/panel';
  7. import {IconEllipsis, IconExpand} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {trackAnalytics} from 'sentry/utils/analytics';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. import usePageFilters from 'sentry/utils/usePageFilters';
  13. import useProjects from 'sentry/utils/useProjects';
  14. import {getAlertsUrl} from 'sentry/views/insights/common/utils/getAlertsUrl';
  15. import {hasInsightsAlerts} from 'sentry/views/insights/common/utils/hasInsightsAlerts';
  16. import {useModuleNameFromUrl} from 'sentry/views/insights/common/utils/useModuleNameFromUrl';
  17. import {Subtitle} from 'sentry/views/performance/landing/widgets/widgets/singleFieldAreaWidget';
  18. export type AlertConfig = {
  19. aggregate: string;
  20. name?: string;
  21. query?: string;
  22. };
  23. type Props = {
  24. children: React.ReactNode;
  25. alertConfigs?: AlertConfig[];
  26. button?: JSX.Element;
  27. className?: string;
  28. subtitle?: React.ReactNode;
  29. title?: React.ReactNode;
  30. };
  31. export default function ChartPanel({
  32. title,
  33. children,
  34. button,
  35. subtitle,
  36. alertConfigs,
  37. className,
  38. }: Props) {
  39. const organization = useOrganization();
  40. const {selection} = usePageFilters();
  41. const {projects} = useProjects();
  42. const project = useMemo(
  43. () =>
  44. selection.projects.length === 1
  45. ? projects.find(p => p.id === `${selection.projects[0]}`)
  46. : undefined,
  47. [projects, selection.projects]
  48. );
  49. const moduleName = useModuleNameFromUrl();
  50. const alertsUrls =
  51. alertConfigs?.map(alertConfig => {
  52. // Alerts only support single project selection and one or all environment
  53. const singleProject = selection.projects.length === 1 && project;
  54. const singleEnvironment = selection.environments.length <= 1;
  55. const alertsUrl =
  56. alertConfig && singleProject && singleEnvironment
  57. ? getAlertsUrl({
  58. project,
  59. orgSlug: organization.slug,
  60. pageFilters: selection,
  61. name: typeof title === 'string' ? title : undefined,
  62. ...alertConfig,
  63. })
  64. : undefined;
  65. const name = alertConfig.name ?? 'Create Alert';
  66. const disabled = !singleProject || !singleEnvironment;
  67. const tooltip = !singleProject
  68. ? t(
  69. 'Alerts are only available for single project selection. Update your project filter to create an alert.'
  70. )
  71. : !singleEnvironment
  72. ? t(
  73. 'Alerts are only available with at most one environment selection. Update your environment filter to create an alert.'
  74. )
  75. : undefined;
  76. return {
  77. key: name,
  78. label: name,
  79. to: alertsUrl,
  80. disabled,
  81. tooltip,
  82. onClick: () => {
  83. trackAnalytics('insight.general.create_alert', {
  84. organization,
  85. chart_name: typeof title === 'string' ? title : undefined,
  86. alert_name: name,
  87. source: moduleName ?? undefined,
  88. });
  89. },
  90. };
  91. }) ?? [];
  92. const dropdownMenuItems = hasInsightsAlerts(organization) ? alertsUrls : [];
  93. return (
  94. <PanelWithNoPadding className={className}>
  95. <PanelBody>
  96. {title && (
  97. <Header data-test-id="chart-panel-header">
  98. {title && (
  99. <ChartLabel>
  100. {typeof title === 'string' ? (
  101. <TextTitleContainer>{title}</TextTitleContainer>
  102. ) : (
  103. title
  104. )}
  105. </ChartLabel>
  106. )}
  107. <MenuContainer>
  108. {button}
  109. {dropdownMenuItems.length > 0 && (
  110. <DropdownMenu
  111. triggerProps={{
  112. 'aria-label': t('Chart Actions'),
  113. size: 'xs',
  114. borderless: true,
  115. showChevron: false,
  116. icon: <IconEllipsis direction="down" size="sm" />,
  117. }}
  118. position="bottom-end"
  119. items={dropdownMenuItems}
  120. />
  121. )}
  122. <Button
  123. aria-label={t('Expand Insight Chart')}
  124. borderless
  125. size="xs"
  126. icon={<IconExpand />}
  127. onClick={() => {
  128. openInsightChartModal({title, children});
  129. }}
  130. />
  131. </MenuContainer>
  132. </Header>
  133. )}
  134. {subtitle && (
  135. <SubtitleContainer>
  136. <Subtitle>{subtitle}</Subtitle>
  137. </SubtitleContainer>
  138. )}
  139. {children}
  140. </PanelBody>
  141. </PanelWithNoPadding>
  142. );
  143. }
  144. const PanelWithNoPadding = styled(Panel)`
  145. margin-bottom: 0;
  146. `;
  147. const TextTitleContainer = styled('div')`
  148. padding: 1px 0;
  149. `;
  150. const SubtitleContainer = styled('div')`
  151. padding-top: ${space(0.5)};
  152. `;
  153. const ChartLabel = styled('div')`
  154. ${p => p.theme.text.cardTitle}
  155. `;
  156. const PanelBody = styled('div')`
  157. padding: ${space(2)};
  158. `;
  159. const Header = styled('div')`
  160. width: 100%;
  161. display: flex;
  162. align-items: center;
  163. justify-content: space-between;
  164. `;
  165. const MenuContainer = styled('span')`
  166. display: flex;
  167. `;