Skip to content

Commit 59474fb

Browse files
roagascttcpergetsantry[bot]
authored
feat(seer): Enable bulk editing seer automation (#91807)
<img width="1369" alt="Screenshot 2025-05-16 at 10 10 08 AM" src="https://github.com/user-attachments/assets/d5e14874-7ac7-4b10-a129-22a671b14a6d" /> <img width="1333" alt="Screenshot 2025-05-16 at 10 10 04 AM" src="https://github.com/user-attachments/assets/3b3afb53-d0a9-4b9b-af93-78393b9495b8" /> <img width="1380" alt="Screenshot 2025-05-16 at 10 09 59 AM" src="https://github.com/user-attachments/assets/b1bf57d3-a187-4e94-a6f7-8dfbf10f1f6c" /> --------- Co-authored-by: Scott Cooper <scttcper@gmail.com> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 1cfa515 commit 59474fb

File tree

2 files changed

+108
-4
lines changed

2 files changed

+108
-4
lines changed

static/gsApp/views/seerAutomation/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ function SeerAutomationRoot() {
2020
return (
2121
<Fragment>
2222
<SentryDocumentTitle title={t('Seer Automation')} orgSlug={organization.slug} />
23-
<SettingsPageHeader title={t('Seer Automation')} />
23+
<SettingsPageHeader
24+
title={t('Seer Automation')}
25+
subtitle={t(
26+
'Seer can automatically find a root cause and solution for incoming issues.'
27+
)}
28+
/>
2429
<ProjectPermissionAlert />
2530

2631
<NoProjectMessage organization={organization}>

static/gsApp/views/seerAutomation/seerAutomationProjectList.tsx

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import {Fragment, useState} from 'react';
22
import styled from '@emotion/styled';
33

4+
import {
5+
addErrorMessage,
6+
addLoadingMessage,
7+
addSuccessMessage,
8+
} from 'sentry/actionCreators/indicator';
49
import {Flex} from 'sentry/components/container/flex';
510
import {ProjectAvatar} from 'sentry/components/core/avatar/projectAvatar';
611
import {Button} from 'sentry/components/core/button';
712
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
13+
import {Checkbox} from 'sentry/components/core/checkbox';
14+
import {DropdownMenu} from 'sentry/components/dropdownMenu';
815
import Link from 'sentry/components/links/link';
916
import LoadingError from 'sentry/components/loadingError';
1017
import LoadingIndicator from 'sentry/components/loadingIndicator';
@@ -17,10 +24,15 @@ import {IconChevron} from 'sentry/icons';
1724
import {t} from 'sentry/locale';
1825
import {space} from 'sentry/styles/space';
1926
import type {Project} from 'sentry/types/project';
20-
import {useDetailedProject} from 'sentry/utils/useDetailedProject';
27+
import {useQueryClient} from 'sentry/utils/queryClient';
28+
import useApi from 'sentry/utils/useApi';
29+
import {
30+
makeDetailedProjectQueryKey,
31+
useDetailedProject,
32+
} from 'sentry/utils/useDetailedProject';
2133
import useOrganization from 'sentry/utils/useOrganization';
2234
import useProjects from 'sentry/utils/useProjects';
23-
import {formatSeerValue} from 'sentry/views/settings/projectSeer';
35+
import {formatSeerValue, SEER_THRESHOLD_MAP} from 'sentry/views/settings/projectSeer';
2436

2537
const PROJECTS_PER_PAGE = 20;
2638

@@ -49,8 +61,11 @@ function ProjectSeerSetting({project, orgSlug}: {orgSlug: string; project: Proje
4961

5062
export function SeerAutomationProjectList() {
5163
const organization = useOrganization();
64+
const api = useApi({persistInFlight: true});
5265
const {projects, fetching, fetchError} = useProjects();
5366
const [page, setPage] = useState(1);
67+
const [selected, setSelected] = useState<Set<string>>(() => new Set());
68+
const queryClient = useQueryClient();
5469

5570
if (fetching) {
5671
return <LoadingIndicator />;
@@ -76,17 +91,94 @@ export function SeerAutomationProjectList() {
7691
setPage(p => p + 1);
7792
};
7893

94+
const allSelected = selected.size === projects.length && projects.length > 0;
95+
const toggleSelectAll = () => {
96+
if (allSelected) {
97+
// Unselect all projects
98+
setSelected(new Set());
99+
} else {
100+
// Select all projects
101+
setSelected(new Set(projects.map(project => project.id)));
102+
}
103+
};
104+
105+
const toggleProject = (projectId: string) => {
106+
setSelected(prev => {
107+
const newSet = new Set(prev);
108+
if (newSet.has(projectId)) {
109+
newSet.delete(projectId);
110+
} else {
111+
newSet.add(projectId);
112+
}
113+
return newSet;
114+
});
115+
};
116+
117+
async function updateProjectsSeerValue(value: string) {
118+
addLoadingMessage('Updating projects...', {duration: 30000});
119+
try {
120+
await Promise.all(
121+
Array.from(selected).map(projectId => {
122+
const project = projects.find(p => p.id === projectId);
123+
if (!project) return Promise.resolve();
124+
return api.requestPromise(`/projects/${organization.slug}/${project.slug}/`, {
125+
method: 'PUT',
126+
data: {autofixAutomationTuning: value},
127+
});
128+
})
129+
);
130+
addSuccessMessage('Projects updated successfully');
131+
} catch (err) {
132+
addErrorMessage('Failed to update some projects');
133+
} finally {
134+
Array.from(selected).forEach(projectId => {
135+
const project = projects.find(p => p.id === projectId);
136+
if (!project) return;
137+
queryClient.invalidateQueries({
138+
queryKey: makeDetailedProjectQueryKey({
139+
orgSlug: organization.slug,
140+
projectSlug: project.slug,
141+
}),
142+
});
143+
});
144+
}
145+
}
146+
147+
const actionMenuItems = SEER_THRESHOLD_MAP.map(key => ({
148+
key,
149+
label: formatSeerValue(key),
150+
onAction: () => updateProjectsSeerValue(key),
151+
}));
152+
79153
return (
80154
<Fragment>
81155
<Panel>
82156
<PanelHeader>
83-
<div>{t('Current Project Settings')}</div>
157+
{selected.size > 0 ? (
158+
<ActionDropdownMenu
159+
items={actionMenuItems}
160+
triggerLabel={t('Set to')}
161+
size="sm"
162+
/>
163+
) : (
164+
<div>{t('Current Project Settings')}</div>
165+
)}
166+
<div style={{marginLeft: 'auto'}}>
167+
<Button size="sm" onClick={toggleSelectAll}>
168+
{allSelected ? t('Unselect All') : t('Select All')}
169+
</Button>
170+
</div>
84171
</PanelHeader>
85172
<PanelBody>
86173
{paginatedProjects.map(project => (
87174
<PanelItem key={project.id}>
88175
<Flex justify="space-between" gap={space(2)} flex={1}>
89176
<Flex gap={space(1)} align="center">
177+
<Checkbox
178+
checked={selected.has(project.id)}
179+
onChange={() => toggleProject(project.id)}
180+
aria-label={t('Toggle project')}
181+
/>
90182
<ProjectAvatar project={project} title={project.slug} />
91183
<Link
92184
to={`/settings/${organization.slug}/projects/${project.slug}/seer/`}
@@ -127,3 +219,10 @@ export function SeerAutomationProjectList() {
127219
const SeerValue = styled('div')`
128220
color: ${p => p.theme.subText};
129221
`;
222+
223+
const ActionDropdownMenu = styled(DropdownMenu)`
224+
[data-test-id='menu-list-item-label'] {
225+
font-weight: normal;
226+
text-transform: none;
227+
}
228+
`;

0 commit comments

Comments
 (0)