Skip to content

Commit 3a1b5a9

Browse files
ref(dynamic-sampling): Use individual AppField per project rate
Replace the single projectRates record field with per-project AppField instances. Each row now gets its own field via form.AppField name={projectRates.${projectId}}, so the form system handles per-field validation, error display, and canSubmit natively. Removes the showErrors/getProjectRateError workaround. Bulk edit uses form.setFieldValue per project instead of updating the record. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 122f41e commit 3a1b5a9

File tree

3 files changed

+71
-72
lines changed

3 files changed

+71
-72
lines changed

static/app/views/settings/dynamicSampling/projectSampling.tsx

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ const projectSamplingSchema = z.object({
3838
projectRates: z.record(z.string(), sampleRateField),
3939
});
4040

41+
export type ProjectSamplingFormData = z.infer<typeof projectSamplingSchema>;
42+
export type ProjectSamplingForm = ReturnType<
43+
typeof useScrapsForm<ProjectSamplingFormData>
44+
>;
45+
4146
export function ProjectSampling() {
4247
const hasAccess = useHasDynamicSamplingWriteAccess();
4348
const [period, setPeriod] = useState<ProjectionSamplePeriod>('24h');
@@ -133,40 +138,31 @@ export function ProjectSampling() {
133138
{sampleCountsQuery.isError ? (
134139
<LoadingError onRetry={sampleCountsQuery.refetch} />
135140
) : (
136-
<form.AppField name="projectRates">
137-
{field => (
138-
<ProjectsEditTable
139-
period={period}
140-
editMode={editMode}
141-
onEditModeChange={setEditMode}
142-
isLoading={sampleRatesQuery.isPending || sampleCountsQuery.isPending}
143-
sampleCounts={sampleCountsQuery.data}
144-
projectRates={field.state.value}
145-
savedProjectRates={savedProjectRates}
146-
onProjectRatesChange={field.handleChange}
147-
showErrors={!field.state.meta.isValid}
148-
actions={
149-
<Fragment>
150-
<Button
151-
disabled={!isDirty || updateSamplingProjectRates.isPending}
152-
onClick={() => {
153-
form.reset();
154-
setEditMode('single');
155-
}}
156-
>
157-
{t('Reset')}
158-
</Button>
159-
<form.SubmitButton
160-
disabled={!hasAccess || !canSubmit}
161-
formNoValidate
162-
>
163-
{t('Apply Changes')}
164-
</form.SubmitButton>
165-
</Fragment>
166-
}
167-
/>
168-
)}
169-
</form.AppField>
141+
<ProjectsEditTable
142+
form={form}
143+
period={period}
144+
editMode={editMode}
145+
onEditModeChange={setEditMode}
146+
isLoading={sampleRatesQuery.isPending || sampleCountsQuery.isPending}
147+
sampleCounts={sampleCountsQuery.data}
148+
savedProjectRates={savedProjectRates}
149+
actions={
150+
<Fragment>
151+
<Button
152+
disabled={!isDirty || updateSamplingProjectRates.isPending}
153+
onClick={() => {
154+
form.reset();
155+
setEditMode('single');
156+
}}
157+
>
158+
{t('Reset')}
159+
</Button>
160+
<form.SubmitButton disabled={!hasAccess || !canSubmit} formNoValidate>
161+
{t('Apply Changes')}
162+
</form.SubmitButton>
163+
</Fragment>
164+
}
165+
/>
170166
)}
171167
<FormActions />
172168
</Fragment>

static/app/views/settings/dynamicSampling/projectsEditTable.tsx

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {Panel} from 'sentry/components/panels/panel';
88
import {t} from 'sentry/locale';
99
import {useProjects} from 'sentry/utils/useProjects';
1010
import {OrganizationSampleRateInput} from 'sentry/views/settings/dynamicSampling/organizationSampleRateInput';
11-
import {sampleRateField} from 'sentry/views/settings/dynamicSampling/organizationSampling';
11+
import type {ProjectSamplingForm} from 'sentry/views/settings/dynamicSampling/projectSampling';
1212
import {ProjectsTable} from 'sentry/views/settings/dynamicSampling/projectsTable';
1313
import {SamplingBreakdown} from 'sentry/views/settings/dynamicSampling/samplingBreakdown';
1414
import {mapArrayToObject} from 'sentry/views/settings/dynamicSampling/utils';
@@ -23,48 +23,34 @@ import type {
2323
interface Props {
2424
actions: React.ReactNode;
2525
editMode: 'single' | 'bulk';
26+
form: ProjectSamplingForm;
2627
isLoading: boolean;
2728
onEditModeChange: (mode: 'single' | 'bulk') => void;
28-
onProjectRatesChange: (
29-
updater:
30-
| Record<string, string>
31-
| ((prev: Record<string, string>) => Record<string, string>)
32-
) => void;
3329
period: ProjectionSamplePeriod;
34-
projectRates: Record<string, string>;
3530
sampleCounts: ProjectSampleCount[];
3631
savedProjectRates: Record<string, string>;
37-
showErrors?: boolean;
3832
}
3933

4034
const EMPTY_ARRAY: any = [];
4135

42-
function getProjectRateError(rate: string): string | undefined {
43-
const result = sampleRateField.safeParse(rate);
44-
if (result.success) {
45-
return undefined;
46-
}
47-
return result.error.issues[0]?.message;
48-
}
49-
5036
export function ProjectsEditTable({
5137
actions,
5238
isLoading: isLoadingProp,
5339
sampleCounts,
5440
editMode,
5541
period,
5642
onEditModeChange,
57-
projectRates,
43+
form,
5844
savedProjectRates,
59-
onProjectRatesChange,
60-
showErrors,
6145
}: Props) {
6246
const {projects, fetching} = useProjects();
6347
const [isBulkEditEnabled, setIsBulkEditEnabled] = useState(false);
6448
const [orgRate, setOrgRate] = useState<string>('');
6549

6650
const projectRateSnapshotRef = useRef<Record<string, string>>({});
6751

52+
const projectRates = form.useStore(s => s.values.projectRates);
53+
6854
const dataByProjectId = useMemo(
6955
() =>
7056
mapArrayToObject({
@@ -77,13 +63,10 @@ export function ProjectsEditTable({
7763

7864
const handleProjectChange = useCallback(
7965
(projectId: string, newRate: string) => {
80-
onProjectRatesChange(prev => ({
81-
...prev,
82-
[projectId]: newRate,
83-
}));
66+
form.setFieldValue(`projectRates.${projectId}`, newRate);
8467
onEditModeChange('single');
8568
},
86-
[onProjectRatesChange, onEditModeChange]
69+
[form, onEditModeChange]
8770
);
8871

8972
const handleOrgChange = useCallback(
@@ -116,11 +99,13 @@ export function ProjectsEditTable({
11699
valueSelector: item => formatPercent(item.sampleRate),
117100
});
118101

119-
onProjectRatesChange(prev => ({...prev, ...newProjectValues}));
102+
for (const [projectId, rate] of Object.entries(newProjectValues)) {
103+
form.setFieldValue(`projectRates.${projectId}`, rate);
104+
}
120105
setOrgRate(newRate);
121106
onEditModeChange('bulk');
122107
},
123-
[dataByProjectId, editMode, onProjectRatesChange, onEditModeChange, projectRates]
108+
[dataByProjectId, editMode, form, onEditModeChange, projectRates]
124109
);
125110

126111
const handleBulkEditChange = useCallback((newIsActive: boolean) => {
@@ -145,12 +130,9 @@ export function ProjectsEditTable({
145130
project,
146131
initialSampleRate: savedProjectRates[project.id]!,
147132
sampleRate: projectRates[project.id]!,
148-
error: showErrors
149-
? getProjectRateError(projectRates[project.id] ?? '')
150-
: undefined,
151133
};
152134
}),
153-
[dataByProjectId, showErrors, savedProjectRates, projects, projectRates]
135+
[dataByProjectId, savedProjectRates, projects, projectRates]
154136
);
155137

156138
const totalSpanCount = useMemo(
@@ -227,6 +209,7 @@ export function ProjectsEditTable({
227209
period={period}
228210
isLoading={isLoading}
229211
items={items}
212+
form={form}
230213
/>
231214
<Footer>{actions}</Footer>
232215
</Fragment>

static/app/views/settings/dynamicSampling/projectsTable.tsx

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
1919
import {oxfordizeArray} from 'sentry/utils/oxfordizeArray';
2020
import {useOrganization} from 'sentry/utils/useOrganization';
2121
import {PercentInput} from 'sentry/views/settings/dynamicSampling/percentInput';
22+
import type {ProjectSamplingForm} from 'sentry/views/settings/dynamicSampling/projectSampling';
2223
import {useHasDynamicSamplingWriteAccess} from 'sentry/views/settings/dynamicSampling/utils/access';
2324
import {parsePercent} from 'sentry/views/settings/dynamicSampling/utils/parsePercent';
2425
import type {
@@ -49,6 +50,7 @@ interface Props {
4950
period: ProjectionSamplePeriod;
5051
rateHeader: React.ReactNode;
5152
canEdit?: boolean;
53+
form?: ProjectSamplingForm;
5254
inputTooltip?: string;
5355
onChange?: (projectId: string, value: string) => void;
5456
}
@@ -65,6 +67,7 @@ export function ProjectsTable({
6567
period,
6668
isLoading,
6769
emptyMessage,
70+
form,
6871
}: Props) {
6972
const hasAccess = useHasDynamicSamplingWriteAccess();
7073
const [tableSort, setTableSort] = useState<'asc' | 'desc'>('desc');
@@ -167,14 +170,31 @@ export function ProjectsTable({
167170
transform: `translateY(${virtualRow.start}px)`,
168171
}}
169172
>
170-
<TableRow
171-
canEdit={canEdit}
172-
onChange={onChange}
173-
inputTooltip={inputTooltip}
174-
toggleExpanded={handleToggleItemExpanded}
175-
hasAccess={hasAccess}
176-
{...item}
177-
/>
173+
{form ? (
174+
<form.AppField name={`projectRates.${item.project.id}`}>
175+
{field => (
176+
<TableRow
177+
canEdit={canEdit}
178+
onChange={onChange}
179+
inputTooltip={inputTooltip}
180+
toggleExpanded={handleToggleItemExpanded}
181+
hasAccess={hasAccess}
182+
{...item}
183+
sampleRate={field.state.value}
184+
error={field.state.meta.errors[0]?.message}
185+
/>
186+
)}
187+
</form.AppField>
188+
) : (
189+
<TableRow
190+
canEdit={canEdit}
191+
onChange={onChange}
192+
inputTooltip={inputTooltip}
193+
toggleExpanded={handleToggleItemExpanded}
194+
hasAccess={hasAccess}
195+
{...item}
196+
/>
197+
)}
178198
</div>
179199
);
180200
})}

0 commit comments

Comments
 (0)