1- import { Image , ImageStatus } from '@linode/api-v4' ;
2- import { APIError } from '@linode/api-v4/lib/types' ;
3- import { Theme } from '@mui/material/styles' ;
1+ import CloseIcon from '@mui/icons-material/Close' ;
42import { useQueryClient } from '@tanstack/react-query' ;
53import { useSnackbar } from 'notistack' ;
64import * as React from 'react' ;
7- import { useHistory } from 'react-router-dom' ;
5+ import { useHistory , useLocation } from 'react-router-dom' ;
6+ import { debounce } from 'throttle-debounce' ;
87import { makeStyles } from 'tss-react/mui' ;
98
109import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel' ;
@@ -13,6 +12,8 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati
1312import { DocumentTitleSegment } from 'src/components/DocumentTitle' ;
1413import { ErrorState } from 'src/components/ErrorState/ErrorState' ;
1514import { Hidden } from 'src/components/Hidden' ;
15+ import { IconButton } from 'src/components/IconButton' ;
16+ import { InputAdornment } from 'src/components/InputAdornment' ;
1617import { LandingHeader } from 'src/components/LandingHeader' ;
1718import { Notice } from 'src/components/Notice/Notice' ;
1819import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter' ;
@@ -23,7 +24,9 @@ import { TableCell } from 'src/components/TableCell';
2324import { TableHead } from 'src/components/TableHead' ;
2425import { TableRow } from 'src/components/TableRow' ;
2526import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty' ;
27+ import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading' ;
2628import { TableSortCell } from 'src/components/TableSortCell' ;
29+ import { TextField } from 'src/components/TextField' ;
2730import { Typography } from 'src/components/Typography' ;
2831import { useOrder } from 'src/hooks/useOrder' ;
2932import { usePagination } from 'src/hooks/usePagination' ;
@@ -41,11 +44,17 @@ import { getErrorStringOrDefault } from 'src/utilities/errorUtils';
4144
4245import { EditImageDrawer } from './EditImageDrawer' ;
4346import ImageRow from './ImageRow' ;
44- import { Handlers as ImageHandlers } from './ImagesActionMenu' ;
4547import { ImagesLandingEmptyState } from './ImagesLandingEmptyState' ;
4648import { RebuildImageDrawer } from './RebuildImageDrawer' ;
4749import { getEventsForImages } from './utils' ;
4850
51+ import type { Handlers as ImageHandlers } from './ImagesActionMenu' ;
52+ import type { Image , ImageStatus } from '@linode/api-v4' ;
53+ import type { APIError } from '@linode/api-v4/lib/types' ;
54+ import type { Theme } from '@mui/material/styles' ;
55+
56+ const searchQueryKey = 'query' ;
57+
4958const useStyles = makeStyles ( ) ( ( theme : Theme ) => ( {
5059 imageTable : {
5160 marginBottom : theme . spacing ( 3 ) ,
@@ -81,6 +90,9 @@ export const ImagesLanding = () => {
8190 const { classes } = useStyles ( ) ;
8291 const history = useHistory ( ) ;
8392 const { enqueueSnackbar } = useSnackbar ( ) ;
93+ const location = useLocation ( ) ;
94+ const queryParams = new URLSearchParams ( location . search ) ;
95+ const imageLabelFromParam = queryParams . get ( searchQueryKey ) ?? '' ;
8496
8597 const queryClient = useQueryClient ( ) ;
8698
@@ -104,9 +116,14 @@ export const ImagesLanding = () => {
104116 [ '+order_by' ] : manualImagesOrderBy ,
105117 } ;
106118
119+ if ( imageLabelFromParam ) {
120+ manualImagesFilter [ 'label' ] = { '+contains' : imageLabelFromParam } ;
121+ }
122+
107123 const {
108124 data : manualImages ,
109125 error : manualImagesError ,
126+ isFetching : manualImagesIsFetching ,
110127 isLoading : manualImagesLoading ,
111128 } = useImagesQuery (
112129 {
@@ -144,9 +161,14 @@ export const ImagesLanding = () => {
144161 [ '+order_by' ] : automaticImagesOrderBy ,
145162 } ;
146163
164+ if ( imageLabelFromParam ) {
165+ automaticImagesFilter [ 'label' ] = { '+contains' : imageLabelFromParam } ;
166+ }
167+
147168 const {
148169 data : automaticImages ,
149170 error : automaticImagesError ,
171+ isFetching : automaticImagesIsFetching ,
150172 isLoading : automaticImagesLoading ,
151173 } = useImagesQuery (
152174 {
@@ -310,6 +332,17 @@ export const ImagesLanding = () => {
310332 ) ;
311333 } ;
312334
335+ const resetSearch = ( ) => {
336+ queryParams . delete ( searchQueryKey ) ;
337+ history . push ( { search : queryParams . toString ( ) } ) ;
338+ } ;
339+
340+ const onSearch = ( e : React . ChangeEvent < HTMLInputElement > ) => {
341+ queryParams . delete ( 'page' ) ;
342+ queryParams . set ( searchQueryKey , e . target . value ) ;
343+ history . push ( { search : queryParams . toString ( ) } ) ;
344+ } ;
345+
313346 const handlers : ImageHandlers = {
314347 onCancelFailed : onCancelFailedClick ,
315348 onDelete : openDialog ,
@@ -350,7 +383,11 @@ export const ImagesLanding = () => {
350383 }
351384
352385 /** Empty States */
353- if ( ! manualImages . data . length && ! automaticImages . data . length ) {
386+ if (
387+ ! manualImages . data . length &&
388+ ! automaticImages . data . length &&
389+ ! imageLabelFromParam
390+ ) {
354391 return renderEmpty ( ) ;
355392 }
356393
@@ -362,6 +399,8 @@ export const ImagesLanding = () => {
362399 < TableRowEmpty colSpan = { 6 } message = { `No Recovery Images to display.` } />
363400 ) ;
364401
402+ const isFetching = manualImagesIsFetching || automaticImagesIsFetching ;
403+
365404 return (
366405 < React . Fragment >
367406 < DocumentTitleSegment segment = "Images" />
@@ -371,6 +410,32 @@ export const ImagesLanding = () => {
371410 onButtonClick = { ( ) => history . push ( '/images/create' ) }
372411 title = "Images"
373412 />
413+ < TextField
414+ InputProps = { {
415+ endAdornment : imageLabelFromParam && (
416+ < InputAdornment position = "end" >
417+ { isFetching && < CircleProgress size = "sm" /> }
418+
419+ < IconButton
420+ aria-label = "Clear"
421+ data-testid = "clear-images-search"
422+ onClick = { resetSearch }
423+ size = "small"
424+ >
425+ < CloseIcon />
426+ </ IconButton >
427+ </ InputAdornment >
428+ ) ,
429+ } }
430+ onChange = { debounce ( 400 , ( e ) => {
431+ onSearch ( e ) ;
432+ } ) }
433+ hideLabel
434+ label = "Search"
435+ placeholder = "Search Images"
436+ sx = { { mb : 2 } }
437+ value = { imageLabelFromParam }
438+ />
374439 < Paper className = { classes . imageTable } >
375440 < div className = { classes . imageTableHeader } >
376441 < Typography variant = "h3" > Custom Images</ Typography >
@@ -483,16 +548,20 @@ export const ImagesLanding = () => {
483548 </ TableRow >
484549 </ TableHead >
485550 < TableBody >
486- { automaticImages . data . length > 0
487- ? automaticImages . data . map ( ( automaticImage ) => (
488- < ImageRow
489- event = { automaticImagesEvents [ automaticImage . id ] }
490- handlers = { handlers }
491- image = { automaticImage }
492- key = { automaticImage . id }
493- />
494- ) )
495- : noAutomaticImages }
551+ { isFetching ? (
552+ < TableRowLoading columns = { 6 } />
553+ ) : automaticImages . data . length > 0 ? (
554+ automaticImages . data . map ( ( automaticImage ) => (
555+ < ImageRow
556+ event = { automaticImagesEvents [ automaticImage . id ] }
557+ handlers = { handlers }
558+ image = { automaticImage }
559+ key = { automaticImage . id }
560+ />
561+ ) )
562+ ) : (
563+ noAutomaticImages
564+ ) }
496565 </ TableBody >
497566 </ Table >
498567 < PaginationFooter
0 commit comments