1
- import { Fragment , useState } from 'react' ;
1
+ import { Fragment , useMemo , useState } from 'react' ;
2
2
import styled from '@emotion/styled' ;
3
3
4
4
import {
@@ -15,13 +15,15 @@ import {Flex} from 'sentry/components/core/layout';
15
15
import { Link } from 'sentry/components/core/link' ;
16
16
import { Tooltip } from 'sentry/components/core/tooltip' ;
17
17
import { DropdownMenu } from 'sentry/components/dropdownMenu' ;
18
+ import { useProjectSeerPreferences } from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences' ;
18
19
import LoadingError from 'sentry/components/loadingError' ;
19
20
import LoadingIndicator from 'sentry/components/loadingIndicator' ;
20
21
import Panel from 'sentry/components/panels/panel' ;
21
22
import PanelBody from 'sentry/components/panels/panelBody' ;
22
23
import PanelHeader from 'sentry/components/panels/panelHeader' ;
23
24
import PanelItem from 'sentry/components/panels/panelItem' ;
24
25
import Placeholder from 'sentry/components/placeholder' ;
26
+ import SearchBar from 'sentry/components/searchBar' ;
25
27
import { IconChevron } from 'sentry/icons' ;
26
28
import { t } from 'sentry/locale' ;
27
29
import { space } from 'sentry/styles/space' ;
@@ -44,7 +46,10 @@ function ProjectSeerSetting({project, orgSlug}: {orgSlug: string; project: Proje
44
46
projectSlug : project . slug ,
45
47
} ) ;
46
48
47
- if ( detailedProject . isPending ) {
49
+ const { preference, isPending : isLoadingPreferences } =
50
+ useProjectSeerPreferences ( project ) ;
51
+
52
+ if ( detailedProject . isPending || isLoadingPreferences ) {
48
53
return (
49
54
< div >
50
55
< Placeholder height = "12px" width = "50px" />
@@ -59,17 +64,21 @@ function ProjectSeerSetting({project, orgSlug}: {orgSlug: string; project: Proje
59
64
const { autofixAutomationTuning = 'off' , seerScannerAutomation = false } =
60
65
detailedProject . data ;
61
66
67
+ const repoCount = preference ?. repositories ?. length || 0 ;
68
+
62
69
return (
63
70
< SeerValue >
64
71
< span >
65
72
< Subheading > { t ( 'Scans' ) } :</ Subheading > { ' ' }
66
73
{ seerScannerAutomation ? t ( 'On' ) : t ( 'Off' ) }
67
74
</ span >
68
- { ' | ' }
69
75
< span >
70
76
< Subheading > { t ( 'Fixes' ) } :</ Subheading > { ' ' }
71
77
{ getSeerLabel ( autofixAutomationTuning , seerScannerAutomation ) }
72
78
</ span >
79
+ < span >
80
+ < Subheading > { t ( 'Repos' ) } :</ Subheading > { repoCount }
81
+ </ span >
73
82
</ SeerValue >
74
83
) ;
75
84
}
@@ -115,6 +124,18 @@ export function SeerAutomationProjectList() {
115
124
const [ page , setPage ] = useState ( 1 ) ;
116
125
const [ selected , setSelected ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
117
126
const queryClient = useQueryClient ( ) ;
127
+ const [ search , setSearch ] = useState ( '' ) ;
128
+
129
+ const filteredProjects = useMemo ( ( ) => {
130
+ return projects . filter ( project =>
131
+ project . slug . toLowerCase ( ) . includes ( search . toLowerCase ( ) )
132
+ ) ;
133
+ } , [ projects , search ] ) ;
134
+
135
+ const handleSearchChange = ( searchQuery : string ) => {
136
+ setSearch ( searchQuery ) ;
137
+ setPage ( 1 ) ; // Reset to first page when search changes
138
+ } ;
118
139
119
140
if ( fetching ) {
120
141
return < LoadingIndicator /> ;
@@ -124,10 +145,10 @@ export function SeerAutomationProjectList() {
124
145
return < LoadingError /> ;
125
146
}
126
147
127
- const totalProjects = projects . length ;
148
+ const totalProjects = filteredProjects . length ;
128
149
const pageStart = ( page - 1 ) * PROJECTS_PER_PAGE ;
129
150
const pageEnd = page * PROJECTS_PER_PAGE ;
130
- const paginatedProjects = projects . slice ( pageStart , pageEnd ) ;
151
+ const paginatedProjects = filteredProjects . slice ( pageStart , pageEnd ) ;
131
152
132
153
const previousDisabled = page <= 1 ;
133
154
const nextDisabled = pageEnd >= totalProjects ;
@@ -140,14 +161,24 @@ export function SeerAutomationProjectList() {
140
161
setPage ( p => p + 1 ) ;
141
162
} ;
142
163
143
- const allSelected = selected . size === projects . length && projects . length > 0 ;
164
+ const allFilteredSelected =
165
+ filteredProjects . length > 0 &&
166
+ filteredProjects . every ( project => selected . has ( project . id ) ) ;
144
167
const toggleSelectAll = ( ) => {
145
- if ( allSelected ) {
146
- // Unselect all projects
147
- setSelected ( new Set ( ) ) ;
168
+ if ( allFilteredSelected ) {
169
+ // Unselect all filtered projects
170
+ setSelected ( prev => {
171
+ const newSet = new Set ( prev ) ;
172
+ filteredProjects . forEach ( project => newSet . delete ( project . id ) ) ;
173
+ return newSet ;
174
+ } ) ;
148
175
} else {
149
- // Select all projects
150
- setSelected ( new Set ( projects . map ( project => project . id ) ) ) ;
176
+ // Select all filtered projects
177
+ setSelected ( prev => {
178
+ const newSet = new Set ( prev ) ;
179
+ filteredProjects . forEach ( project => newSet . add ( project . id ) ) ;
180
+ return newSet ;
181
+ } ) ;
151
182
}
152
183
} ;
153
184
@@ -244,6 +275,13 @@ export function SeerAutomationProjectList() {
244
275
245
276
return (
246
277
< Fragment >
278
+ < SearchWrapper >
279
+ < SearchBar
280
+ query = { search }
281
+ onChange = { handleSearchChange }
282
+ placeholder = { t ( 'Search projects' ) }
283
+ />
284
+ </ SearchWrapper >
247
285
< Panel >
248
286
< PanelHeader hasButtons >
249
287
{ selected . size > 0 ? (
@@ -264,20 +302,25 @@ export function SeerAutomationProjectList() {
264
302
) }
265
303
< div style = { { marginLeft : 'auto' } } >
266
304
< Button size = "sm" onClick = { toggleSelectAll } >
267
- { allSelected ? t ( 'Unselect All' ) : t ( 'Select All' ) }
305
+ { allFilteredSelected ? t ( 'Unselect All' ) : t ( 'Select All' ) }
268
306
</ Button >
269
307
</ div >
270
308
</ PanelHeader >
271
309
< PanelBody >
310
+ { filteredProjects . length === 0 && search && (
311
+ < div style = { { padding : space ( 2 ) , textAlign : 'center' , color : '#888' } } >
312
+ { t ( 'No projects found matching "%(search)s"' , { search} ) }
313
+ </ div >
314
+ ) }
272
315
{ paginatedProjects . map ( project => (
273
316
< PanelItem key = { project . id } >
274
- < Flex justify = "space-between" gap = { space ( 2 ) } flex = { 1 } >
317
+ < Flex justify = "space-between" align = "center" gap = { space ( 2 ) } flex = { 1 } >
275
318
< Flex gap = { space ( 1 ) } align = "center" >
276
319
< Tooltip
277
320
title = { t ( 'You do not have permission to edit this project' ) }
278
321
disabled = { projectsWithWriteAccess . includes ( project ) }
279
322
>
280
- < Checkbox
323
+ < StyledCheckbox
281
324
checked = { selected . has ( project . id ) }
282
325
onChange = { ( ) => toggleProject ( project . id ) }
283
326
aria-label = { t ( 'Toggle project' ) }
@@ -321,8 +364,15 @@ export function SeerAutomationProjectList() {
321
364
) ;
322
365
}
323
366
367
+ const SearchWrapper = styled ( 'div' ) `
368
+ margin-bottom: ${ space ( 2 ) } ;
369
+ ` ;
370
+
324
371
const SeerValue = styled ( 'div' ) `
325
372
color: ${ p => p . theme . subText } ;
373
+ display: flex;
374
+ justify-content: flex-end;
375
+ gap: ${ space ( 4 ) } ;
326
376
` ;
327
377
328
378
const ActionDropdownMenu = styled ( DropdownMenu ) `
@@ -331,3 +381,9 @@ const ActionDropdownMenu = styled(DropdownMenu)`
331
381
text-transform: none;
332
382
}
333
383
` ;
384
+
385
+ const StyledCheckbox = styled ( Checkbox ) `
386
+ margin-bottom: 0;
387
+ padding-bottom: 0;
388
+ padding-top: ${ space ( 0.5 ) } ;
389
+ ` ;
0 commit comments