1
1
import { Fragment , useState } from 'react' ;
2
2
import styled from '@emotion/styled' ;
3
3
4
+ import {
5
+ addErrorMessage ,
6
+ addLoadingMessage ,
7
+ addSuccessMessage ,
8
+ } from 'sentry/actionCreators/indicator' ;
4
9
import { Flex } from 'sentry/components/container/flex' ;
5
10
import { ProjectAvatar } from 'sentry/components/core/avatar/projectAvatar' ;
6
11
import { Button } from 'sentry/components/core/button' ;
7
12
import { ButtonBar } from 'sentry/components/core/button/buttonBar' ;
13
+ import { Checkbox } from 'sentry/components/core/checkbox' ;
14
+ import { DropdownMenu } from 'sentry/components/dropdownMenu' ;
8
15
import Link from 'sentry/components/links/link' ;
9
16
import LoadingError from 'sentry/components/loadingError' ;
10
17
import LoadingIndicator from 'sentry/components/loadingIndicator' ;
@@ -17,10 +24,15 @@ import {IconChevron} from 'sentry/icons';
17
24
import { t } from 'sentry/locale' ;
18
25
import { space } from 'sentry/styles/space' ;
19
26
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' ;
21
33
import useOrganization from 'sentry/utils/useOrganization' ;
22
34
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' ;
24
36
25
37
const PROJECTS_PER_PAGE = 20 ;
26
38
@@ -49,8 +61,11 @@ function ProjectSeerSetting({project, orgSlug}: {orgSlug: string; project: Proje
49
61
50
62
export function SeerAutomationProjectList ( ) {
51
63
const organization = useOrganization ( ) ;
64
+ const api = useApi ( { persistInFlight : true } ) ;
52
65
const { projects, fetching, fetchError} = useProjects ( ) ;
53
66
const [ page , setPage ] = useState ( 1 ) ;
67
+ const [ selected , setSelected ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
68
+ const queryClient = useQueryClient ( ) ;
54
69
55
70
if ( fetching ) {
56
71
return < LoadingIndicator /> ;
@@ -76,17 +91,94 @@ export function SeerAutomationProjectList() {
76
91
setPage ( p => p + 1 ) ;
77
92
} ;
78
93
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
+
79
153
return (
80
154
< Fragment >
81
155
< Panel >
82
156
< 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 >
84
171
</ PanelHeader >
85
172
< PanelBody >
86
173
{ paginatedProjects . map ( project => (
87
174
< PanelItem key = { project . id } >
88
175
< Flex justify = "space-between" gap = { space ( 2 ) } flex = { 1 } >
89
176
< 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
+ />
90
182
< ProjectAvatar project = { project } title = { project . slug } />
91
183
< Link
92
184
to = { `/settings/${ organization . slug } /projects/${ project . slug } /seer/` }
@@ -127,3 +219,10 @@ export function SeerAutomationProjectList() {
127
219
const SeerValue = styled ( 'div' ) `
128
220
color: ${ p => p . theme . subText } ;
129
221
` ;
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