Skip to content

Commit 6eae0f8

Browse files
committed
feat(seer): Allow bulk-editing Code Review triggers
1 parent f77120e commit 6eae0f8

File tree

3 files changed

+247
-67
lines changed

3 files changed

+247
-67
lines changed

static/app/components/repositories/useBulkUpdateRepositorySettings.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {getRepositoryWithSettingsQueryKey} from 'sentry/components/repositories/useRepositoryWithSettings';
22
import type {RepositoryWithSettings} from 'sentry/types/integrations';
3+
import type {CodeReviewTrigger} from 'sentry/types/seer';
34
import {
45
fetchMutation,
56
useMutation,
@@ -20,7 +21,7 @@ type RepositorySettings =
2021
enabledCodeReview?: never;
2122
}
2223
| {
23-
codeReviewTriggers: string[];
24+
codeReviewTriggers: CodeReviewTrigger[];
2425
enabledCodeReview: boolean;
2526
repositoryIds: string[];
2627
};

static/gsApp/views/seerAutomation/components/repoTable/seerRepoTable.tsx

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -132,33 +132,34 @@ export function SeerRepoTable() {
132132
isFetchingNextPage,
133133
} = result;
134134

135-
const {mutate: mutateRepositorySettings} = useBulkUpdateRepositorySettings({
136-
onSuccess: mutations => {
137-
const mutationMap = new Map(mutations.map(m => [m.id, m]));
138-
queryClient.setQueryData(queryOptions.queryKey, prev => {
139-
if (!prev) {
140-
return prev;
141-
}
142-
return {
143-
...prev,
144-
pages: prev.pages.map(page => ({
145-
...page,
146-
json: page.json.map(repo => mutationMap.get(repo.id) ?? repo),
147-
})),
148-
};
149-
});
150-
},
151-
onSettled: mutations => {
152-
queryClient.invalidateQueries({
153-
queryKey: getSeerOnboardingCheckQueryOptions({organization}).queryKey,
154-
});
155-
(mutations ?? []).forEach(mutation => {
135+
const {mutate: mutateRepositorySettings, mutateAsync: mutateRepositorySettingsAsync} =
136+
useBulkUpdateRepositorySettings({
137+
onSuccess: mutations => {
138+
const mutationMap = new Map(mutations.map(m => [m.id, m]));
139+
queryClient.setQueryData(queryOptions.queryKey, prev => {
140+
if (!prev) {
141+
return prev;
142+
}
143+
return {
144+
...prev,
145+
pages: prev.pages.map(page => ({
146+
...page,
147+
json: page.json.map(repo => mutationMap.get(repo.id) ?? repo),
148+
})),
149+
};
150+
});
151+
},
152+
onSettled: mutations => {
156153
queryClient.invalidateQueries({
157-
queryKey: getRepositoryWithSettingsQueryKey(organization, mutation.id),
154+
queryKey: getSeerOnboardingCheckQueryOptions({organization}).queryKey,
158155
});
159-
});
160-
},
161-
});
156+
(mutations ?? []).forEach(mutation => {
157+
queryClient.invalidateQueries({
158+
queryKey: getRepositoryWithSettingsQueryKey(organization, mutation.id),
159+
});
160+
});
161+
},
162+
});
162163

163164
const knownIds = useMemo(
164165
() => repositories?.map(repository => repository.id) ?? [],
@@ -207,8 +208,9 @@ export function SeerRepoTable() {
207208
gridColumns={GRID_COLUMNS}
208209
isFetchingNextPage={isFetchingNextPage}
209210
isPending={isPending}
210-
mutateRepositorySettings={mutateRepositorySettings}
211+
mutateRepositorySettings={mutateRepositorySettingsAsync}
211212
onSortClick={setSort}
213+
repositories={repositories ?? []}
212214
sort={sort}
213215
/>
214216
{isPending ? (

static/gsApp/views/seerAutomation/components/repoTable/seerRepoTableHeader.tsx

Lines changed: 217 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import {Fragment} from 'react';
1+
import {Fragment, useMemo, useState} from 'react';
22
import styled from '@emotion/styled';
33

44
import {Alert} from '@sentry/scraps/alert';
55
import {Checkbox} from '@sentry/scraps/checkbox';
6+
import {CompactSelect} from '@sentry/scraps/compactSelect';
67
import {Flex} from '@sentry/scraps/layout';
8+
import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';
79

8-
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
9-
import {DropdownMenu} from 'sentry/components/dropdownMenu';
10+
import {
11+
addErrorMessage,
12+
addLoadingMessage,
13+
addSuccessMessage,
14+
} from 'sentry/actionCreators/indicator';
1015
import {QuestionTooltip} from 'sentry/components/questionTooltip';
1116
import type {useBulkUpdateRepositorySettings} from 'sentry/components/repositories/useBulkUpdateRepositorySettings';
1217
import {SimpleTable} from 'sentry/components/tables/simpleTable';
1318
import {t, tct, tn} from 'sentry/locale';
19+
import type {RepositoryWithSettings} from 'sentry/types/integrations';
20+
import type {CodeReviewTrigger} from 'sentry/types/seer';
1421
import {parseQueryKey} from 'sentry/utils/api/apiQueryKey';
1522
import type {Sort} from 'sentry/utils/discover/fields';
1623
import {useListItemCheckboxContext} from 'sentry/utils/list/useListItemCheckboxState';
@@ -21,8 +28,11 @@ interface Props {
2128
gridColumns: string;
2229
isFetchingNextPage: boolean;
2330
isPending: boolean;
24-
mutateRepositorySettings: ReturnType<typeof useBulkUpdateRepositorySettings>['mutate'];
31+
mutateRepositorySettings: ReturnType<
32+
typeof useBulkUpdateRepositorySettings
33+
>['mutateAsync'];
2534
onSortClick: (key: Sort) => void;
35+
repositories: RepositoryWithSettings[];
2636
sort: Sort;
2737
}
2838

@@ -53,6 +63,7 @@ export function SeerRepoTableHeader({
5363
isPending,
5464
mutateRepositorySettings,
5565
onSortClick,
66+
repositories,
5667
sort,
5768
}: Props) {
5869
const canWrite = useCanWriteSettings();
@@ -71,34 +82,160 @@ export function SeerRepoTableHeader({
7182
: undefined;
7283
const queryString = queryOptions?.query?.query;
7384

74-
const handleBulkCodeReview = (enabledCodeReview: boolean) => {
85+
const selectedRepos = useMemo(() => {
86+
return repositories.filter(repo => selectedIds.includes(repo.id));
87+
}, [repositories, selectedIds]);
88+
89+
const currentCodeReviewValue = useMemo(() => {
90+
const someEnabled = selectedRepos.some(repo => repo?.settings?.enabledCodeReview);
91+
const someDisabled = selectedRepos.some(
92+
repo => repo?.settings?.enabledCodeReview === false
93+
);
94+
if (someEnabled && someDisabled) {
95+
return undefined;
96+
}
97+
if (someEnabled) {
98+
return 'enabled_code_review:enabled';
99+
}
100+
if (someDisabled) {
101+
return 'enabled_code_review:disabled';
102+
}
103+
return undefined;
104+
}, [selectedRepos]);
105+
106+
const currentTriggersValue = useMemo((): CodeReviewTrigger[] => {
107+
const someOnReadyForReview = selectedRepos.every(repo =>
108+
repo?.settings?.codeReviewTriggers?.includes('on_ready_for_review')
109+
);
110+
const someOnNewCommit = selectedRepos.every(repo =>
111+
repo?.settings?.codeReviewTriggers?.includes('on_new_commit')
112+
);
113+
return [
114+
...(someOnReadyForReview ? ['on_ready_for_review' as const] : []),
115+
...(someOnNewCommit ? ['on_new_commit' as const] : []),
116+
];
117+
}, [selectedRepos]);
118+
119+
const [isBulkUpdating, setIsBulkUpdating] = useState(false);
120+
121+
const handleBulkCodeReview = async (enabledCodeReview: boolean) => {
75122
const repositoryIds = selectedIds === 'all' ? knownIds : selectedIds;
76-
mutateRepositorySettings(
77-
{
78-
enabledCodeReview,
79-
repositoryIds,
80-
},
81-
{
82-
onError: () => {
83-
addErrorMessage(
84-
tn(
85-
'Failed to update code review for %s repository',
86-
'Failed to update code review for %s repositories',
87-
repositoryIds.length
88-
)
89-
);
90-
},
91-
onSuccess: () => {
92-
addSuccessMessage(
93-
tn(
94-
'Code review updated for %s repository',
95-
'Code review updated for %s repositories',
96-
repositoryIds.length
97-
)
98-
);
99-
},
100-
}
123+
setIsBulkUpdating(true);
124+
addLoadingMessage(
125+
tn(
126+
'Updating code review for %s repository…',
127+
'Updating code review for %s repositories…',
128+
repositoryIds.length
129+
)
101130
);
131+
try {
132+
await mutateRepositorySettings({enabledCodeReview, repositoryIds});
133+
addSuccessMessage(
134+
tn(
135+
'Code review updated for %s repository',
136+
'Code review updated for %s repositories',
137+
repositoryIds.length
138+
)
139+
);
140+
} catch {
141+
addErrorMessage(
142+
tn(
143+
'Failed to update code review for %s repository',
144+
'Failed to update code review for %s repositories',
145+
repositoryIds.length
146+
)
147+
);
148+
} finally {
149+
setIsBulkUpdating(false);
150+
}
151+
};
152+
153+
const handleBulkTriggers = async ({
154+
added,
155+
removed,
156+
}: {
157+
added: CodeReviewTrigger | undefined;
158+
removed: CodeReviewTrigger | undefined;
159+
}) => {
160+
const promises: Array<Promise<unknown>> = [];
161+
162+
if (added) {
163+
const repoIdsWithZeroTriggers: string[] = [];
164+
const repoIdsWithOneTrigger: string[] = [];
165+
for (const repo of selectedRepos) {
166+
if (repo.settings?.codeReviewTriggers?.length === 0) {
167+
repoIdsWithZeroTriggers.push(repo.id);
168+
} else if (!repo.settings?.codeReviewTriggers.includes(added)) {
169+
repoIdsWithOneTrigger.push(repo.id);
170+
}
171+
}
172+
// Some items start with 0 triggers, they'll be saved with 1 new trigger
173+
if (repoIdsWithZeroTriggers.length > 0) {
174+
promises.push(
175+
mutateRepositorySettings({
176+
codeReviewTriggers: [added],
177+
repositoryIds: repoIdsWithZeroTriggers,
178+
})
179+
);
180+
}
181+
// Some items start with 1 trigger, they'll be saved with 1 new trigger for a total of 2
182+
if (repoIdsWithOneTrigger.length > 0) {
183+
promises.push(
184+
mutateRepositorySettings({
185+
codeReviewTriggers: ['on_new_commit', 'on_ready_for_review'],
186+
repositoryIds: repoIdsWithOneTrigger,
187+
})
188+
);
189+
}
190+
}
191+
if (removed) {
192+
const repoIdsWithOneTrigger: string[] = [];
193+
const repoIdsWithTwoTriggers: string[] = [];
194+
for (const repo of selectedRepos) {
195+
if (repo.settings?.codeReviewTriggers?.length === 2) {
196+
repoIdsWithTwoTriggers.push(repo.id);
197+
} else if (repo.settings?.codeReviewTriggers?.includes(removed)) {
198+
repoIdsWithOneTrigger.push(repo.id);
199+
}
200+
}
201+
// Some items start with 2 triggers, we'll remove one
202+
const remainingTrigger =
203+
removed === 'on_new_commit' ? 'on_ready_for_review' : 'on_new_commit';
204+
if (repoIdsWithTwoTriggers.length > 0) {
205+
promises.push(
206+
mutateRepositorySettings({
207+
codeReviewTriggers: [remainingTrigger],
208+
repositoryIds: repoIdsWithTwoTriggers,
209+
})
210+
);
211+
}
212+
// Some items start with 1 trigger, we'll remove it
213+
if (repoIdsWithOneTrigger.length > 0) {
214+
promises.push(
215+
mutateRepositorySettings({
216+
codeReviewTriggers: [],
217+
repositoryIds: repoIdsWithOneTrigger,
218+
})
219+
);
220+
}
221+
}
222+
223+
if (promises.length === 0) {
224+
return;
225+
}
226+
227+
setIsBulkUpdating(true);
228+
addLoadingMessage(t('Updating triggers…'));
229+
230+
const results = await Promise.allSettled(promises);
231+
const hasError = results.some(r => r.status === 'rejected');
232+
setIsBulkUpdating(false);
233+
234+
if (hasError) {
235+
addErrorMessage(t('Failed to update triggers'));
236+
} else {
237+
addSuccessMessage(t('Triggers updated'));
238+
}
102239
};
103240

104241
return (
@@ -147,22 +284,62 @@ export function SeerRepoTableHeader({
147284
/>
148285
</TableCellFirst>
149286
<TableCellsRemainingContent align="center" gap="md">
150-
<DropdownMenu
151-
isDisabled={!canWrite}
287+
<CompactSelect
288+
disabled={!canWrite}
289+
size="xs"
290+
trigger={props => (
291+
<OverlayTrigger.Button {...props}>
292+
{t('Code Review')}
293+
</OverlayTrigger.Button>
294+
)}
295+
options={[
296+
{
297+
value: 'enabled_code_review:enabled',
298+
label: t('Enable'),
299+
disabled: isBulkUpdating,
300+
},
301+
{
302+
value: 'enabled_code_review:disabled',
303+
label: t('Disable'),
304+
disabled: isBulkUpdating,
305+
},
306+
]}
307+
value={currentCodeReviewValue}
308+
onChange={option => {
309+
if (option.value === 'enabled_code_review:enabled') {
310+
handleBulkCodeReview(true);
311+
} else {
312+
handleBulkCodeReview(false);
313+
}
314+
}}
315+
/>
316+
317+
<CompactSelect<CodeReviewTrigger>
318+
disabled={!canWrite}
319+
multiple
152320
size="xs"
153-
items={[
321+
trigger={props => (
322+
<OverlayTrigger.Button {...props}>{t('Triggers')}</OverlayTrigger.Button>
323+
)}
324+
options={[
154325
{
155-
key: 'on',
156-
label: t('On'),
157-
onAction: () => handleBulkCodeReview(true),
326+
value: 'on_ready_for_review',
327+
label: t('On Ready for Review'),
328+
disabled: isBulkUpdating,
158329
},
159330
{
160-
key: 'off',
161-
label: t('Off'),
162-
onAction: () => handleBulkCodeReview(false),
331+
value: 'on_new_commit',
332+
label: t('On New Commit'),
333+
disabled: isBulkUpdating,
163334
},
164335
]}
165-
triggerLabel={t('Code Review')}
336+
value={currentTriggersValue}
337+
onChange={option => {
338+
const value = option.map(v => v.value);
339+
const added = value.findLast(v => !currentTriggersValue.includes(v));
340+
const removed = currentTriggersValue.findLast(v => !value.includes(v));
341+
handleBulkTriggers({added, removed});
342+
}}
166343
/>
167344
</TableCellsRemainingContent>
168345
</TableHeader>

0 commit comments

Comments
 (0)