metricsExtractionRuleCreateModal.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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 {useMetricsCardinality} from 'sentry/utils/metrics/useMetricsCardinality';
  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. projectId?: string | number;
  29. }
  30. const INITIAL_DATA: FormData = {
  31. spanAttribute: null,
  32. aggregates: ['count'],
  33. tags: ['release', 'environment'],
  34. conditions: [createCondition()],
  35. };
  36. export function MetricsExtractionRuleCreateModal({
  37. Header,
  38. Body,
  39. closeModal,
  40. CloseButton,
  41. projectId: projectIdProp,
  42. }: Props & ModalRenderProps) {
  43. const {projects} = useProjects();
  44. const {selection} = usePageFilters();
  45. const initialProjectId = useMemo(() => {
  46. if (projectIdProp) {
  47. return projectIdProp;
  48. }
  49. if (selection.projects.length === 1 && selection.projects[0] !== -1) {
  50. return projects.find(p => p.id === String(selection.projects[0]))?.id;
  51. }
  52. return undefined;
  53. // eslint-disable-next-line react-hooks/exhaustive-deps
  54. }, []);
  55. const [projectId, setProjectId] = useState<string | number | undefined>(
  56. initialProjectId
  57. );
  58. const projectOptions = useMemo(() => {
  59. const nonMemberProjects: Project[] = [];
  60. const memberProjects: Project[] = [];
  61. projects
  62. .filter(
  63. project =>
  64. selection.projects.length === 0 ||
  65. selection.projects.includes(parseInt(project.id, 10))
  66. )
  67. .forEach(project =>
  68. project.isMember ? memberProjects.push(project) : nonMemberProjects.push(project)
  69. );
  70. return [
  71. {
  72. label: t('My Projects'),
  73. options: memberProjects.map(p => ({
  74. value: p.id,
  75. label: p.slug,
  76. leadingItems: <ProjectBadge project={p} avatarSize={16} hideName disableLink />,
  77. })),
  78. },
  79. {
  80. label: t('All Projects'),
  81. options: nonMemberProjects.map(p => ({
  82. value: p.id,
  83. label: p.slug,
  84. leadingItems: <ProjectBadge project={p} avatarSize={16} hideName disableLink />,
  85. })),
  86. },
  87. ];
  88. }, [selection.projects, projects]);
  89. return (
  90. <Fragment>
  91. <Header>
  92. <h4>{t('Configure Metric')}</h4>
  93. </Header>
  94. <CloseButton />
  95. <Body>
  96. {initialProjectId === undefined ? (
  97. <ProjectSelectionWrapper>
  98. <label htmlFor="project-select">{t('Project')}</label>
  99. <SelectControl
  100. id="project-select"
  101. placeholder={t('Select a project')}
  102. options={projectOptions}
  103. value={projectId}
  104. onChange={({value}) => setProjectId(value)}
  105. />
  106. </ProjectSelectionWrapper>
  107. ) : null}
  108. {projectId ? <FormWrapper projectId={projectId} closeModal={closeModal} /> : null}
  109. </Body>
  110. </Fragment>
  111. );
  112. }
  113. function FormWrapper({
  114. closeModal,
  115. projectId,
  116. }: {
  117. closeModal: () => void;
  118. projectId: string | number;
  119. }) {
  120. const organization = useOrganization();
  121. const createExtractionRuleMutation = useCreateMetricsExtractionRules(
  122. organization.slug,
  123. projectId
  124. );
  125. const {data: cardinality} = useMetricsCardinality({
  126. projects: [projectId],
  127. });
  128. const handleSubmit = useCallback(
  129. (
  130. data: FormData,
  131. onSubmitSuccess: (data: FormData) => void,
  132. onSubmitError: (error: any) => void
  133. ) => {
  134. const extractionRule: MetricsExtractionRule = {
  135. spanAttribute: data.spanAttribute!,
  136. tags: data.tags,
  137. aggregates: data.aggregates.flatMap(explodeAggregateGroup),
  138. unit: 'none',
  139. conditions: data.conditions,
  140. projectId: Number(projectId),
  141. };
  142. createExtractionRuleMutation.mutate(
  143. {
  144. metricsExtractionRules: [extractionRule],
  145. },
  146. {
  147. onSuccess: () => {
  148. onSubmitSuccess(data);
  149. addSuccessMessage(t('Metric extraction rule created'));
  150. closeModal();
  151. },
  152. onError: error => {
  153. const message = error?.responseJSON?.detail
  154. ? (error.responseJSON.detail as string)
  155. : t('Unable to save your changes.');
  156. onSubmitError(message);
  157. addErrorMessage(message);
  158. },
  159. }
  160. );
  161. onSubmitSuccess(data);
  162. },
  163. [closeModal, projectId, createExtractionRuleMutation]
  164. );
  165. return (
  166. <MetricsExtractionRuleForm
  167. initialData={INITIAL_DATA}
  168. projectId={projectId}
  169. submitLabel={t('Add Metric')}
  170. cancelLabel={t('Cancel')}
  171. onCancel={closeModal}
  172. onSubmit={handleSubmit}
  173. cardinality={cardinality}
  174. requireChanges
  175. />
  176. );
  177. }
  178. const ProjectSelectionWrapper = styled('div')`
  179. padding-bottom: ${space(2)};
  180. padding-left: ${space(2)};
  181. :not(:last-child) {
  182. border-bottom: 1px solid ${p => p.theme.innerBorder};
  183. }
  184. `;
  185. export const modalCss = css`
  186. width: 100%;
  187. max-width: 900px;
  188. `;
  189. export function openExtractionRuleCreateModal(props: Props, options?: ModalOptions) {
  190. openModal(
  191. modalProps => <MetricsExtractionRuleCreateModal {...props} {...modalProps} />,
  192. {
  193. modalCss,
  194. ...options,
  195. }
  196. );
  197. }