metricsExtractionRuleCreateModal.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import {Fragment, useCallback, useMemo, useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  5. import {
  6. type ModalOptions,
  7. type ModalRenderProps,
  8. openModal,
  9. } from 'sentry/actionCreators/modal';
  10. import SelectControl from 'sentry/components/forms/controls/selectControl';
  11. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {MetricsExtractionRule} from 'sentry/types/metrics';
  15. import type {Project} from 'sentry/types/project';
  16. import {useCardinalityLimitedMetricVolume} from 'sentry/utils/metrics/useCardinalityLimitedMetricVolume';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import usePageFilters from 'sentry/utils/usePageFilters';
  19. import useProjects from 'sentry/utils/useProjects';
  20. import {
  21. createCondition,
  22. explodeAggregateGroup,
  23. type FormData,
  24. MetricsExtractionRuleForm,
  25. } from 'sentry/views/settings/projectMetrics/metricsExtractionRuleForm';
  26. import {useCreateMetricsExtractionRules} from 'sentry/views/settings/projectMetrics/utils/useMetricsExtractionRules';
  27. interface Props {
  28. initialData?: Partial<FormData>;
  29. projectId?: string | number;
  30. }
  31. export const INITIAL_DATA: FormData = {
  32. spanAttribute: null,
  33. unit: 'none',
  34. aggregates: ['count'],
  35. tags: ['release', 'environment'],
  36. conditions: [createCondition()],
  37. };
  38. export function MetricsExtractionRuleCreateModal({
  39. Header,
  40. Body,
  41. closeModal,
  42. CloseButton,
  43. initialData: initalDataProp = {},
  44. projectId: projectIdProp,
  45. }: Props & ModalRenderProps) {
  46. const {projects} = useProjects();
  47. const {selection} = usePageFilters();
  48. const initialData = useMemo(() => {
  49. return {
  50. ...INITIAL_DATA,
  51. ...initalDataProp,
  52. };
  53. }, [initalDataProp]);
  54. const initialProjectId = useMemo(() => {
  55. if (projectIdProp) {
  56. return projectIdProp;
  57. }
  58. if (selection.projects.length === 1 && selection.projects[0] !== -1) {
  59. return projects.find(p => p.id === String(selection.projects[0]))?.id;
  60. }
  61. return undefined;
  62. // eslint-disable-next-line react-hooks/exhaustive-deps
  63. }, []);
  64. const [projectId, setProjectId] = useState<string | number | undefined>(
  65. initialProjectId
  66. );
  67. const projectOptions = useMemo(() => {
  68. const nonMemberProjects: Project[] = [];
  69. const memberProjects: Project[] = [];
  70. projects
  71. .filter(
  72. project =>
  73. selection.projects.length === 0 ||
  74. selection.projects.includes(parseInt(project.id, 10))
  75. )
  76. .forEach(project =>
  77. project.isMember ? memberProjects.push(project) : nonMemberProjects.push(project)
  78. );
  79. return [
  80. {
  81. label: t('My Projects'),
  82. options: memberProjects.map(p => ({
  83. value: p.id,
  84. label: p.slug,
  85. leadingItems: <ProjectBadge project={p} avatarSize={16} hideName disableLink />,
  86. })),
  87. },
  88. {
  89. label: t('All Projects'),
  90. options: nonMemberProjects.map(p => ({
  91. value: p.id,
  92. label: p.slug,
  93. leadingItems: <ProjectBadge project={p} avatarSize={16} hideName disableLink />,
  94. })),
  95. },
  96. ];
  97. }, [selection.projects, projects]);
  98. return (
  99. <Fragment>
  100. <Header>
  101. <h4>{t('Create Metric')}</h4>
  102. </Header>
  103. <CloseButton />
  104. <Body>
  105. <p>
  106. {t(
  107. "Set up the metric you'd like to track and we'll collect it for you from future data."
  108. )}
  109. </p>
  110. {initialProjectId === undefined ? (
  111. <ProjectSelectionWrapper>
  112. <label htmlFor="project-select">{t('Project')}</label>
  113. <SelectControl
  114. id="project-select"
  115. placeholder={t('Select a project')}
  116. options={projectOptions}
  117. value={projectId}
  118. onChange={({value}) => setProjectId(value)}
  119. stacked={false}
  120. />
  121. </ProjectSelectionWrapper>
  122. ) : null}
  123. {projectId ? (
  124. <FormWrapper
  125. initialData={initialData}
  126. projectId={projectId}
  127. closeModal={closeModal}
  128. />
  129. ) : null}
  130. </Body>
  131. </Fragment>
  132. );
  133. }
  134. function FormWrapper({
  135. closeModal,
  136. projectId,
  137. initialData,
  138. }: {
  139. closeModal: () => void;
  140. initialData: FormData;
  141. projectId: string | number;
  142. }) {
  143. const organization = useOrganization();
  144. const createExtractionRuleMutation = useCreateMetricsExtractionRules(
  145. organization.slug,
  146. projectId
  147. );
  148. const {data: cardinality} = useCardinalityLimitedMetricVolume({
  149. projects: [projectId],
  150. });
  151. const handleSubmit = useCallback(
  152. (
  153. data: FormData,
  154. onSubmitSuccess: (data: FormData) => void,
  155. onSubmitError: (error: any) => void
  156. ) => {
  157. const extractionRule: MetricsExtractionRule = {
  158. spanAttribute: data.spanAttribute!,
  159. tags: data.tags,
  160. aggregates: data.aggregates.flatMap(explodeAggregateGroup),
  161. unit: data.unit,
  162. conditions: data.conditions,
  163. projectId: Number(projectId),
  164. // Will be set by the backend
  165. createdById: null,
  166. dateAdded: '',
  167. dateUpdated: '',
  168. };
  169. createExtractionRuleMutation.mutate(
  170. {
  171. metricsExtractionRules: [extractionRule],
  172. },
  173. {
  174. onSuccess: () => {
  175. onSubmitSuccess(data);
  176. addSuccessMessage(t('Metric extraction rule created'));
  177. closeModal();
  178. },
  179. onError: error => {
  180. const message = error?.responseJSON?.detail
  181. ? (error.responseJSON.detail as string)
  182. : t('Unable to save your changes.');
  183. onSubmitError(message);
  184. addErrorMessage(message);
  185. },
  186. }
  187. );
  188. onSubmitSuccess(data);
  189. },
  190. [closeModal, projectId, createExtractionRuleMutation]
  191. );
  192. return (
  193. <MetricsExtractionRuleForm
  194. initialData={initialData}
  195. projectId={projectId}
  196. submitLabel={t('Add Metric')}
  197. cancelLabel={t('Cancel')}
  198. onCancel={closeModal}
  199. onSubmit={handleSubmit}
  200. cardinality={cardinality}
  201. submitDisabled={createExtractionRuleMutation.isLoading}
  202. />
  203. );
  204. }
  205. const ProjectSelectionWrapper = styled('div')`
  206. padding-bottom: ${space(2)};
  207. & > label {
  208. color: ${p => p.theme.gray300};
  209. }
  210. `;
  211. export const modalCss = css`
  212. width: 100%;
  213. max-width: 900px;
  214. `;
  215. export function openExtractionRuleCreateModal(props: Props, options?: ModalOptions) {
  216. openModal(
  217. modalProps => <MetricsExtractionRuleCreateModal {...props} {...modalProps} />,
  218. {
  219. modalCss,
  220. ...options,
  221. }
  222. );
  223. }