projectsEditTable.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import {Fragment, useCallback, useMemo} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import partition from 'lodash/partition';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import Panel from 'sentry/components/panels/panel';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {formatNumberWithDynamicDecimalPoints} from 'sentry/utils/number/formatNumberWithDynamicDecimalPoints';
  10. import useProjects from 'sentry/utils/useProjects';
  11. import {PercentInput} from 'sentry/views/settings/dynamicSampling/percentInput';
  12. import {ProjectsTable} from 'sentry/views/settings/dynamicSampling/projectsTable';
  13. import {SamplingBreakdown} from 'sentry/views/settings/dynamicSampling/samplingBreakdown';
  14. import {projectSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/projectSamplingForm';
  15. import type {ProjectSampleCount} from 'sentry/views/settings/dynamicSampling/utils/useProjectSampleCounts';
  16. interface Props {
  17. isLoading: boolean;
  18. sampleCounts: ProjectSampleCount[];
  19. }
  20. const {useFormField} = projectSamplingForm;
  21. const EMPTY_ARRAY = [];
  22. export function ProjectsEditTable({isLoading: isLoadingProp, sampleCounts}: Props) {
  23. const {projects, fetching} = useProjects();
  24. const {value, initialValue, error, onChange} = useFormField('projectRates');
  25. const dataByProjectId = sampleCounts.reduce(
  26. (acc, item) => {
  27. acc[item.project.id] = item;
  28. return acc;
  29. },
  30. {} as Record<string, (typeof sampleCounts)[0]>
  31. );
  32. const items = useMemo(
  33. () =>
  34. projects.map(project => {
  35. const item = dataByProjectId[project.id] as
  36. | (typeof dataByProjectId)[string]
  37. | undefined;
  38. return {
  39. id: project.slug,
  40. name: project.slug,
  41. count: item?.count || 0,
  42. ownCount: item?.ownCount || 0,
  43. subProjects: item?.subProjects ?? EMPTY_ARRAY,
  44. project: project,
  45. initialSampleRate: initialValue[project.id],
  46. sampleRate: value[project.id],
  47. error: error?.[project.id],
  48. };
  49. }),
  50. [dataByProjectId, error, initialValue, projects, value]
  51. );
  52. const [activeItems, inactiveItems] = partition(items, item => item.count > 0);
  53. const handleChange = useCallback(
  54. (projectId: string, newRate: string) => {
  55. onChange(prev => ({
  56. ...prev,
  57. [projectId]: newRate,
  58. }));
  59. },
  60. [onChange]
  61. );
  62. // weighted average of all projects' sample rates
  63. const totalSpans = items.reduce((acc, item) => acc + item.count, 0);
  64. const projectedOrgRate = useMemo(() => {
  65. const totalSampledSpans = items.reduce(
  66. (acc, item) => acc + item.count * Number(value[item.project.id] ?? 100),
  67. 0
  68. );
  69. return totalSampledSpans / totalSpans;
  70. }, [items, value, totalSpans]);
  71. const breakdownSampleRates = useMemo(
  72. () =>
  73. Object.entries(value).reduce(
  74. (acc, [projectId, rate]) => {
  75. acc[projectId] = Number(rate) / 100;
  76. return acc;
  77. },
  78. {} as Record<string, number>
  79. ),
  80. [value]
  81. );
  82. const isLoading = fetching || isLoadingProp;
  83. return (
  84. <Fragment>
  85. <BreakdownPanel>
  86. {isLoading ? (
  87. <LoadingIndicator
  88. css={css`
  89. margin: ${space(4)} 0;
  90. `}
  91. />
  92. ) : (
  93. <Fragment>
  94. <ProjectedOrgRateWrapper>
  95. {t('Projected Organization Rate')}
  96. <PercentInput
  97. type="number"
  98. disabled
  99. min={0}
  100. max={100}
  101. size="sm"
  102. value={formatNumberWithDynamicDecimalPoints(projectedOrgRate, 2)}
  103. />
  104. </ProjectedOrgRateWrapper>
  105. <Divider />
  106. <SamplingBreakdown
  107. sampleCounts={sampleCounts}
  108. sampleRates={breakdownSampleRates}
  109. />
  110. </Fragment>
  111. )}
  112. </BreakdownPanel>
  113. <ProjectsTable
  114. canEdit
  115. onChange={handleChange}
  116. emptyMessage={t('No active projects found in the selected period.')}
  117. isLoading={isLoading}
  118. items={activeItems}
  119. inactiveItems={inactiveItems}
  120. />
  121. </Fragment>
  122. );
  123. }
  124. const BreakdownPanel = styled(Panel)`
  125. margin-bottom: ${space(3)};
  126. padding: ${space(2)};
  127. `;
  128. const ProjectedOrgRateWrapper = styled('label')`
  129. display: flex;
  130. align-items: center;
  131. justify-content: space-between;
  132. flex-wrap: wrap;
  133. gap: ${space(1)};
  134. font-weight: ${p => p.theme.fontWeightNormal};
  135. `;
  136. const Divider = styled('hr')`
  137. margin: ${space(2)} -${space(2)};
  138. border: none;
  139. border-top: 1px solid ${p => p.theme.innerBorder};
  140. `;