Skip to content

Commit 0be518e

Browse files
authored
feat(seer): Enable sorting seer autofix project list (#112250)
For this I had to properly disable the 'no handoff' option finally. Because sorting that into the list isn't right anymore, and would take a bit of extra code. So that's gone.
1 parent b32d74d commit 0be518e

File tree

6 files changed

+115
-198
lines changed

6 files changed

+115
-198
lines changed

static/app/views/settings/seer/seerAgentHooks.spec.tsx

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type {CodingAgentIntegration} from 'sentry/components/events/autofix/useA
1313
import {ProjectsStore} from 'sentry/stores/projectsStore';
1414
import {useQueryClient} from 'sentry/utils/queryClient';
1515
import {
16-
useAgentOptions,
1716
useBulkMutateCreatePr,
1817
useMutateCreatePr,
1918
useMutateSelectedAgent,
@@ -35,65 +34,31 @@ describe('seerAgentHooks', () => {
3534
ProjectsStore.reset();
3635
});
3736

38-
describe('useAgentOptions', () => {
39-
it('returns Seer, integration options', () => {
40-
const integrations: CodingAgentIntegration[] = [
41-
{id: '42', name: 'Cursor', provider: 'cursor'},
42-
];
43-
44-
const {result} = renderHookWithProviders(useAgentOptions, {
45-
initialProps: {integrations},
46-
organization,
47-
});
48-
49-
const options = result.current;
50-
expect(options).toHaveLength(3);
51-
expect(options[0]).toEqual({value: 'seer', label: expect.any(String)});
52-
expect(options[1]).toMatchObject({
53-
value: {id: '42', name: 'Cursor', provider: 'cursor'},
54-
label: 'Cursor',
55-
});
56-
expect(options[2]).toEqual({value: 'none', label: 'No Handoff'});
57-
});
58-
59-
it('filters out integrations without id', () => {
60-
const integrations: CodingAgentIntegration[] = [
61-
{id: null, name: 'No Id', provider: 'other'},
62-
{id: '1', name: 'With Id', provider: 'cursor'},
63-
];
64-
65-
const {result} = renderHookWithProviders(useAgentOptions, {
66-
initialProps: {integrations},
67-
organization,
68-
});
69-
70-
const options = result.current;
71-
expect(options).toHaveLength(3);
72-
expect(options[1]!.value).toMatchObject({id: '1', name: 'With Id'});
73-
});
74-
});
75-
7637
describe('useSelectedAgentFromProjectSettings', () => {
77-
it('returns "none" when project autofixAutomationTuning is off', () => {
78-
const p = ProjectFixture({...project, autofixAutomationTuning: 'off'});
79-
38+
it('returns "seer" when no automation_handoff integration_id', () => {
8039
const {result} = renderHookWithProviders(useSelectedAgentFromProjectSettings, {
8140
initialProps: {
82-
preference: {repositories: []},
83-
project: p,
41+
preference: {
42+
repositories: [],
43+
},
8444
integrations: [],
8545
},
8646
organization,
8747
});
8848

89-
expect(result.current).toBe('none');
49+
expect(result.current).toBe('seer');
9050
});
9151

92-
it('returns "seer" when no automation_handoff integration_id', () => {
52+
it('returns "seer" when no automation_handoff integration_id, ignoring autofixAutomationTuning', () => {
9353
const {result} = renderHookWithProviders(useSelectedAgentFromProjectSettings, {
9454
initialProps: {
95-
preference: {repositories: []},
96-
project,
55+
preference: {
56+
projectId: '1',
57+
autofixAutomationTuning: 'off',
58+
automatedRunStoppingPoint: 'code_changes',
59+
automation_handoff: undefined,
60+
repositories: [],
61+
},
9762
integrations: [],
9863
},
9964
organization,
@@ -117,7 +82,6 @@ describe('seerAgentHooks', () => {
11782
integration_id: 99,
11883
},
11984
},
120-
project,
12185
integrations,
12286
},
12387
organization,
@@ -128,7 +92,7 @@ describe('seerAgentHooks', () => {
12892
});
12993

13094
describe('useSelectedAgentFromBulkSettings', () => {
131-
it('returns "none" when autofixAutomationTuning is off', () => {
95+
it('returns "seer" when automationHandoff undefined, doesnt look at autofixAutomationTuning', () => {
13296
const {result} = renderHookWithProviders(useSelectedAgentFromBulkSettings, {
13397
initialProps: {
13498
autofixSettings: {
@@ -143,7 +107,7 @@ describe('seerAgentHooks', () => {
143107
organization,
144108
});
145109

146-
expect(result.current).toBe('none');
110+
expect(result.current).toBe('seer');
147111
});
148112

149113
it('returns "seer" when no automationHandoff integration_id', () => {

static/app/views/settings/seer/seerAgentHooks.tsx

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,39 +23,14 @@ import {fetchDataQuery, fetchMutation, useQueryClient} from 'sentry/utils/queryC
2323
import {RequestError} from 'sentry/utils/requestError/requestError';
2424
import {useOrganization} from 'sentry/utils/useOrganization';
2525

26-
export function useAgentOptions({
27-
integrations,
28-
}: {
29-
integrations: CodingAgentIntegration[];
30-
}) {
31-
return useMemo(() => {
32-
return [
33-
{value: 'seer' as const, label: t('Seer Agent')},
34-
...integrations
35-
.filter(integration => integration.id)
36-
.map(integration => ({
37-
value: integration,
38-
label: integration.name,
39-
})),
40-
{value: 'none' as const, label: t('No Handoff')},
41-
];
42-
}, [integrations]);
43-
}
44-
4526
export function useSelectedAgentFromProjectSettings({
4627
integrations,
4728
preference,
48-
project,
4929
}: {
5030
integrations: CodingAgentIntegration[];
5131
preference: ProjectSeerPreferences;
52-
project: Project;
5332
}) {
5433
return useMemo(() => {
55-
// If we have autofixAutomationTuning==OFF then 'none' is picked
56-
if (project.autofixAutomationTuning === 'off') {
57-
return 'none';
58-
}
5934
// If we have nothing in preferences, then we have Seer
6035
if (!preference?.automation_handoff?.integration_id) {
6136
return 'seer';
@@ -65,11 +40,7 @@ export function useSelectedAgentFromProjectSettings({
6540
integration =>
6641
integration.id === String(preference.automation_handoff?.integration_id)
6742
);
68-
}, [
69-
preference?.automation_handoff?.integration_id,
70-
project.autofixAutomationTuning,
71-
integrations,
72-
]);
43+
}, [preference.automation_handoff?.integration_id, integrations]);
7344
}
7445

7546
export function useSelectedAgentFromBulkSettings({
@@ -80,10 +51,6 @@ export function useSelectedAgentFromBulkSettings({
8051
integrations: CodingAgentIntegration[];
8152
}) {
8253
return useMemo(() => {
83-
// If we have autofixAutomationTuning==OFF then 'none' is picked
84-
if (autofixSettings.autofixAutomationTuning === 'off') {
85-
return 'none';
86-
}
8754
// If we have nothing in preferences, then we have Seer
8855
if (!autofixSettings?.automationHandoff?.integration_id) {
8956
return 'seer';
@@ -93,11 +60,7 @@ export function useSelectedAgentFromBulkSettings({
9360
integration =>
9461
integration.id === String(autofixSettings.automationHandoff?.integration_id)
9562
);
96-
}, [
97-
autofixSettings.automationHandoff?.integration_id,
98-
autofixSettings.autofixAutomationTuning,
99-
integrations,
100-
]);
63+
}, [autofixSettings.automationHandoff?.integration_id, integrations]);
10164
}
10265

10366
function useApplyOptimisticUpdate({project}: {project: Project}) {

static/gsApp/views/seerAutomation/components/projectDetails/autofixAgent.tsx

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import {t, tct} from 'sentry/locale';
2020
import type {Project} from 'sentry/types/project';
2121
import {useMutation, useQuery, useQueryClient} from 'sentry/utils/queryClient';
2222
import {useOrganization} from 'sentry/utils/useOrganization';
23+
import {useFetchAgentOptions} from 'sentry/views/settings/seer/overview/utils/seerPreferredAgent';
2324
import {
2425
getProjectStoppingPointMutationOptions,
2526
getProjectStoppingPointValue,
2627
useFetchStoppingPointOptions,
2728
} from 'sentry/views/settings/seer/overview/utils/seerStoppingPoint';
2829
import {
29-
useAgentOptions,
3030
useMutateSelectedAgent,
3131
useSelectedAgentFromProjectSettings,
3232
} from 'sentry/views/settings/seer/seerAgentHooks';
@@ -44,14 +44,11 @@ function AgentSpecificFields({
4444
integration,
4545
...props
4646
}: Props & {
47-
integration: 'seer' | 'none' | CodingAgentIntegration;
47+
integration: 'seer' | CodingAgentIntegration;
4848
}) {
4949
if (integration === 'seer') {
5050
return <SeerAgentSettings {...props} />;
5151
}
52-
if (integration === 'none') {
53-
return null;
54-
}
5552
if (integration.provider === 'cursor' || integration.provider === 'claude_code') {
5653
return <CodingAgentSettings integration={integration} {...props} />;
5754
}
@@ -68,10 +65,9 @@ export function AutofixAgent({canWrite, preference, project}: Props) {
6865
...organizationIntegrationsCodingAgents(organization),
6966
select: data => data.json.integrations ?? [],
7067
});
71-
const options = useAgentOptions({integrations: integrations ?? []});
68+
const options = useFetchAgentOptions({organization});
7269
const selected = useSelectedAgentFromProjectSettings({
7370
preference,
74-
project,
7571
integrations: integrations ?? [],
7672
});
7773
const mutateSelectedAgent = useMutateSelectedAgent({project});
@@ -106,37 +102,29 @@ export function AutofixAgent({canWrite, preference, project}: Props) {
106102
),
107103
}
108104
)}
109-
options={options}
105+
options={options.data ?? []}
110106
value={selected}
111-
onChange={(integration: 'seer' | 'none' | CodingAgentIntegration) => {
107+
onChange={(integration: 'seer' | CodingAgentIntegration) => {
112108
mutateSelectedAgent(integration, {
113109
onSuccess: () =>
114110
addSuccessMessage(
115-
integration === 'none'
116-
? t('Removed coding agent')
117-
: tct('Started using [name] as coding agent', {
118-
name: (
119-
<strong>
120-
{integration === 'seer'
121-
? t('Seer Agent')
122-
: integration.name}
123-
</strong>
124-
),
125-
})
111+
tct('Started using [name] as coding agent', {
112+
name: (
113+
<strong>
114+
{integration === 'seer' ? t('Seer Agent') : integration.name}
115+
</strong>
116+
),
117+
})
126118
),
127119
onError: () =>
128120
addErrorMessage(
129-
integration === 'none'
130-
? t('Failed to update coding agent')
131-
: tct('Failed to set [name] as coding agent', {
132-
name: (
133-
<strong>
134-
{integration === 'seer'
135-
? t('Seer Agent')
136-
: integration.name}
137-
</strong>
138-
),
139-
})
121+
tct('Failed to set [name] as coding agent', {
122+
name: (
123+
<strong>
124+
{integration === 'seer' ? t('Seer Agent') : integration.name}
125+
</strong>
126+
),
127+
})
140128
),
141129
});
142130
}}
@@ -150,7 +138,7 @@ export function AutofixAgent({canWrite, preference, project}: Props) {
150138
/>
151139
) : null}
152140

153-
{selected && selected !== 'none' ? (
141+
{selected ? (
154142
<StoppingPointField
155143
agent={selected}
156144
canWrite={canWrite}

static/gsApp/views/seerAutomation/components/projectTable/seerProjectTable.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {ApiQueryKey} from 'sentry/utils/queryClient';
2525
import {parseAsSort} from 'sentry/utils/url/parseAsSort';
2626
import {useOrganization} from 'sentry/utils/useOrganization';
2727
import {useProjects} from 'sentry/utils/useProjects';
28+
import {useFetchAgentOptions} from 'sentry/views/settings/seer/overview/utils/seerPreferredAgent';
2829

2930
import {ProjectTableHeader} from 'getsentry/views/seerAutomation/components/projectTable/seerProjectTableHeader';
3031
import {SeerProjectTableRow} from 'getsentry/views/seerAutomation/components/projectTable/seerProjectTableRow';
@@ -34,6 +35,8 @@ export function SeerProjectTable() {
3435
const organization = useOrganization();
3536
const {projects, fetching, fetchError} = useProjects();
3637

38+
const agentOptions = useFetchAgentOptions({organization});
39+
3740
const autofixSettingsQueryOptions = bulkAutofixAutomationSettingsInfiniteOptions({
3841
organization,
3942
});
@@ -128,19 +131,24 @@ export function SeerProjectTable() {
128131
: b.name.localeCompare(a.name);
129132
}
130133

131-
// TODO: if we can bulk-fetch all the preferences, then it'll be easier to sort by fixes, pr creation, and repos
132-
// if (sort.field === 'fixes') {
133-
// return a.slug.localeCompare(b.slug);
134-
// }
135-
// if (sort.field === 'pr_creation') {
136-
// return a.platform.localeCompare(b.platform);
137-
// }
138-
// if (sort.field === 'repos') {
139-
// return a.status.localeCompare(b.status);
140-
// }
134+
const aSettings = autofixSettingsByProjectId.get(a.id);
135+
const bSettings = autofixSettingsByProjectId.get(b.id);
136+
if (sort.field === 'agent') {
137+
const aAgent = aSettings?.automationHandoff?.target ?? 'seer';
138+
const bAgent = bSettings?.automationHandoff?.target ?? 'seer';
139+
return sort.kind === 'asc'
140+
? aAgent.localeCompare(bAgent)
141+
: bAgent.localeCompare(aAgent);
142+
}
143+
144+
if (sort.field === 'repo_count') {
145+
return sort.kind === 'asc'
146+
? (aSettings?.reposCount ?? 0) - (bSettings?.reposCount ?? 0)
147+
: (bSettings?.reposCount ?? 0) - (aSettings?.reposCount ?? 0);
148+
}
141149
return 0;
142150
});
143-
}, [projects, sort]);
151+
}, [projects, sort, autofixSettingsByProjectId]);
144152

145153
const filteredProjects = useMemo(() => {
146154
const lowerCase = searchTerm?.toLowerCase() ?? '';
@@ -216,6 +224,7 @@ export function SeerProjectTable() {
216224
integrations={integrations ?? []}
217225
isPendingIntegrations={isPendingIntegrations}
218226
project={project}
227+
agentOptions={agentOptions}
219228
/>
220229
))
221230
)}

static/gsApp/views/seerAutomation/components/projectTable/seerProjectTableHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ interface Props {
3434

3535
const COLUMNS = [
3636
{title: t('Project'), key: 'project', sortKey: 'project'},
37-
{title: t('Autofix Handoff'), key: 'fixes'},
37+
{title: t('Agent'), key: 'fixes', sortKey: 'agent'},
3838
{
3939
title: (
4040
<Flex gap="sm" align="center">
@@ -48,7 +48,7 @@ const COLUMNS = [
4848
),
4949
key: 'pr_creation',
5050
},
51-
{title: t('Repos'), key: 'repos'},
51+
{title: t('Repos'), key: 'repos', sortKey: 'repo_count'},
5252
];
5353

5454
function getMutationCallbacks(count: number) {

0 commit comments

Comments
 (0)