metricsExtractionRuleCreateModal.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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. 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. {initialProjectId === undefined ? (
  106. <ProjectSelectionWrapper>
  107. <label htmlFor="project-select">{t('Project')}</label>
  108. <SelectControl
  109. id="project-select"
  110. placeholder={t('Select a project')}
  111. options={projectOptions}
  112. value={projectId}
  113. onChange={({value}) => setProjectId(value)}
  114. stacked={false}
  115. />
  116. </ProjectSelectionWrapper>
  117. ) : null}
  118. {projectId ? (
  119. <FormWrapper
  120. initialData={initialData}
  121. projectId={projectId}
  122. closeModal={closeModal}
  123. />
  124. ) : null}
  125. </Body>
  126. </Fragment>
  127. );
  128. }
  129. function FormWrapper({
  130. closeModal,
  131. projectId,
  132. initialData,
  133. }: {
  134. closeModal: () => void;
  135. initialData: FormData;
  136. projectId: string | number;
  137. }) {
  138. const organization = useOrganization();
  139. const createExtractionRuleMutation = useCreateMetricsExtractionRules(
  140. organization.slug,
  141. projectId
  142. );
  143. const {data: cardinality} = useCardinalityLimitedMetricVolume({
  144. projects: [projectId],
  145. });
  146. const handleSubmit = useCallback(
  147. (
  148. data: FormData,
  149. onSubmitSuccess: (data: FormData) => void,
  150. onSubmitError: (error: any) => void
  151. ) => {
  152. const extractionRule: MetricsExtractionRule = {
  153. spanAttribute: data.spanAttribute!,
  154. tags: data.tags,
  155. aggregates: data.aggregates.flatMap(explodeAggregateGroup),
  156. unit: data.unit,
  157. conditions: data.conditions,
  158. projectId: Number(projectId),
  159. // Will be set by the backend
  160. createdById: null,
  161. dateAdded: '',
  162. dateUpdated: '',
  163. };
  164. createExtractionRuleMutation.mutate(
  165. {
  166. metricsExtractionRules: [extractionRule],
  167. },
  168. {
  169. onSuccess: () => {
  170. onSubmitSuccess(data);
  171. addSuccessMessage(t('Metric extraction rule created'));
  172. closeModal();
  173. },
  174. onError: error => {
  175. const message = error?.responseJSON?.detail
  176. ? (error.responseJSON.detail as string)
  177. : t('Unable to save your changes.');
  178. onSubmitError(message);
  179. addErrorMessage(message);
  180. },
  181. }
  182. );
  183. onSubmitSuccess(data);
  184. },
  185. [closeModal, projectId, createExtractionRuleMutation]
  186. );
  187. return (
  188. <MetricsExtractionRuleForm
  189. initialData={initialData}
  190. projectId={projectId}
  191. submitLabel={t('Add Metric')}
  192. cancelLabel={t('Cancel')}
  193. onCancel={closeModal}
  194. onSubmit={handleSubmit}
  195. cardinality={cardinality}
  196. submitDisabled={createExtractionRuleMutation.isLoading}
  197. />
  198. );
  199. }
  200. const ProjectSelectionWrapper = styled('div')`
  201. padding-bottom: ${space(2)};
  202. & > label {
  203. color: ${p => p.theme.gray300};
  204. }
  205. `;
  206. export const modalCss = css`
  207. width: 100%;
  208. max-width: 900px;
  209. `;
  210. export function openExtractionRuleCreateModal(props: Props, options?: ModalOptions) {
  211. openModal(
  212. modalProps => <MetricsExtractionRuleCreateModal {...props} {...modalProps} />,
  213. {
  214. modalCss,
  215. ...options,
  216. }
  217. );
  218. }