11import React , { useEffect , useState } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
33
4- import { Cards , CardsProps , MultiselectCSD , Popover , PropertyFilter } from 'components' ;
4+ import { Alert , Cards , CardsProps , MultiselectCSD , Popover , PropertyFilter } from 'components' ;
55
66import { useCollection } from 'hooks' ;
77import { useGetGpusListQuery } from 'services/gpu' ;
@@ -30,7 +30,7 @@ const getRequestParams = ({
3030 group_by ?: TGpuGroupBy [ ] ;
3131} ) : TGpusListQueryParams => {
3232 const gpuCountMinMax = rangeToObject ( gpu_count ?? '' ) ;
33- const gpuMemoryMinMax = rangeToObject ( gpu_memory ?? '' ) ;
33+ const gpuMemoryMinMax = rangeToObject ( gpu_memory ?? '' , { requireUnit : true } ) ;
3434
3535 return {
3636 project_name,
@@ -50,29 +50,30 @@ const getRequestParams = ({
5050 // disk: { size: { min: 100.0 } },
5151 gpu : {
5252 ...( gpu_name ?. length ? { name : gpu_name } : { } ) ,
53- ...( gpuCountMinMax ? { count : gpuCountMinMax } : { } ) ,
54- ...( gpuMemoryMinMax ? { memory : gpuMemoryMinMax } : { } ) ,
53+ ...( gpuCountMinMax ? { count : gpuCountMinMax as unknown as TRange } : { } ) ,
54+ ...( gpuMemoryMinMax ? { memory : gpuMemoryMinMax as unknown as TRange } : { } ) ,
5555 } ,
5656 } ,
5757 spot_policy,
5858 volumes : [ ] ,
5959 files : [ ] ,
6060 setup : [ ] ,
61- ...( backend ?. length ? { backends : backend } : { } ) ,
61+ ...( backend ?. length ? { backends : backend as TBackendType [ ] } : { } ) ,
6262 } ,
6363 profile : { name : 'default' , default : false } ,
6464 ssh_key_pub : '(dummy)' ,
6565 } ,
6666 } ;
6767} ;
6868
69- type OfferListProps = Pick < CardsProps , 'variant' | 'header' | 'onSelectionChange' | 'selectedItems' | 'selectionType' > &
70- Pick < UseFiltersArgs , 'permanentFilters' | 'defaultFilters' > & {
71- withSearchParams ?: boolean ;
72- disabled ?: boolean ;
73- onChangeProjectName ?: ( value : string ) => void ;
74- onChangeBackendFilter ?: ( backends : string [ ] ) => void ;
75- } ;
69+ type OfferListProps = Pick < CardsProps , 'variant' | 'header' | 'onSelectionChange' | 'selectedItems' | 'selectionType' > & {
70+ permanentFilters ?: UseFiltersArgs [ 'permanentFilters' ] ;
71+ defaultFilters ?: UseFiltersArgs [ 'defaultFilters' ] ;
72+ withSearchParams ?: boolean ;
73+ disabled ?: boolean ;
74+ onChangeProjectName ?: ( value : string ) => void ;
75+ onChangeBackendFilter ?: ( backends : string [ ] ) => void ;
76+ } ;
7677
7778export const OfferList : React . FC < OfferListProps > = ( {
7879 withSearchParams,
@@ -86,7 +87,7 @@ export const OfferList: React.FC<OfferListProps> = ({
8687 const { t } = useTranslation ( ) ;
8788 const [ requestParams , setRequestParams ] = useState < TGpusListQueryParams | undefined > ( ) ;
8889
89- const { data, isLoading, isFetching } = useGetGpusListQuery (
90+ const { data, error , isError , isLoading, isFetching } = useGetGpusListQuery (
9091 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
9192 // @ts -expect-error
9293 requestParams ,
@@ -121,12 +122,16 @@ export const OfferList: React.FC<OfferListProps> = ({
121122 } , [ JSON . stringify ( filteringRequestParams ) , groupBy ] ) ;
122123
123124 useEffect ( ( ) => {
124- onChangeProjectName ?.( filteringRequestParams . project_name ?? '' ) ;
125+ const projectName = typeof filteringRequestParams . project_name === 'string' ? filteringRequestParams . project_name : '' ;
126+ onChangeProjectName ?.( projectName ) ;
125127 } , [ filteringRequestParams . project_name ] ) ;
126128
127129 useEffect ( ( ) => {
128130 const backend = filteringRequestParams . backend ;
129- onChangeBackendFilter ?.( backend ? ( Array . isArray ( backend ) ? backend : [ backend ] ) : [ ] ) ;
131+ const backendValues = backend
132+ ? ( Array . isArray ( backend ) ? backend : [ backend ] ) . filter ( ( value ) : value is string => typeof value === 'string' )
133+ : [ ] ;
134+ onChangeBackendFilter ?.( backendValues ) ;
130135 } , [ filteringRequestParams . backend ] ) ;
131136
132137 const { renderEmptyMessage, renderNoMatchMessage } = useEmptyMessages ( {
@@ -228,56 +233,66 @@ export const OfferList: React.FC<OfferListProps> = ({
228233 ] . filter ( Boolean ) as CardsProps . CardDefinition < IGpu > [ 'sections' ] ;
229234
230235 return (
231- < Cards
232- { ...collectionProps }
233- { ...props }
234- entireCardClickable
235- items = { disabled ? [ ] : items }
236- empty = { disabled ? ' ' : undefined }
237- cardDefinition = { {
238- header : ( gpu ) => gpu . name ,
239- sections,
240- } }
241- loading = { ! disabled && ( isLoading || isFetching ) }
242- loadingText = { t ( 'common.loading' ) }
243- stickyHeader = { true }
244- filter = {
245- disabled ? undefined : (
246- < div className = { styles . selectFilters } >
247- < div className = { styles . propertyFilter } >
248- < PropertyFilter
249- disabled = { isLoading || isFetching }
250- query = { propertyFilterQuery }
251- onChange = { onChangePropertyFilter }
252- expandToViewport
253- hideOperations
254- i18nStrings = { {
255- clearFiltersText : t ( 'common.clearFilter' ) ,
256- filteringAriaLabel : t ( 'offer.filter_property_placeholder' ) ,
257- filteringPlaceholder : t ( 'offer.filter_property_placeholder' ) ,
258- operationAndText : 'and' ,
259- enteredTextLabel : ( value ) => `Use: ${ value } ` ,
260- } }
261- filteringOptions = { filteringOptions }
262- filteringProperties = { filteringProperties }
263- filteringStatusType = { filteringStatusType }
264- onLoadItems = { handleLoadItems }
265- />
266- </ div >
236+ < >
237+ { ! disabled && isError && (
238+ < Alert type = "error" header = "Error" >
239+ { 'data' in ( error as object ) && ( error as { data ?: { detail ?: { msg ?: string } [ ] } } ) . data ?. detail ?. [ 0 ] ?. msg
240+ ? ( error as { data ?: { detail ?: { msg ?: string } [ ] } } ) . data ?. detail ?. [ 0 ] ?. msg
241+ : t ( 'common.server_error' , { error : 'Unknown error' } ) }
242+ </ Alert >
243+ ) }
267244
268- < div className = { styles . filterField } >
269- < MultiselectCSD
270- placeholder = { t ( 'offer.groupBy' ) }
271- onChange = { onChangeGroupBy }
272- options = { groupByOptions }
273- selectedOptions = { groupBy }
274- expandToViewport = { true }
275- disabled = { isLoading || isFetching }
276- />
245+ < Cards
246+ { ...collectionProps }
247+ { ...props }
248+ entireCardClickable
249+ items = { disabled ? [ ] : items }
250+ empty = { disabled ? ' ' : undefined }
251+ cardDefinition = { {
252+ header : ( gpu ) => gpu . name ,
253+ sections,
254+ } }
255+ loading = { ! disabled && ( isLoading || isFetching ) }
256+ loadingText = { t ( 'common.loading' ) }
257+ stickyHeader = { true }
258+ filter = {
259+ disabled ? undefined : (
260+ < div className = { styles . selectFilters } >
261+ < div className = { styles . propertyFilter } >
262+ < PropertyFilter
263+ disabled = { isLoading || isFetching }
264+ query = { propertyFilterQuery }
265+ onChange = { onChangePropertyFilter }
266+ expandToViewport
267+ hideOperations
268+ i18nStrings = { {
269+ clearFiltersText : t ( 'common.clearFilter' ) ,
270+ filteringAriaLabel : t ( 'offer.filter_property_placeholder' ) ,
271+ filteringPlaceholder : t ( 'offer.filter_property_placeholder' ) ,
272+ operationAndText : 'and' ,
273+ enteredTextLabel : ( value ) => `Use: ${ value } ` ,
274+ } }
275+ filteringOptions = { filteringOptions }
276+ filteringProperties = { filteringProperties }
277+ filteringStatusType = { filteringStatusType }
278+ onLoadItems = { handleLoadItems }
279+ />
280+ </ div >
281+
282+ < div className = { styles . filterField } >
283+ < MultiselectCSD
284+ placeholder = { t ( 'offer.groupBy' ) }
285+ onChange = { onChangeGroupBy }
286+ options = { groupByOptions }
287+ selectedOptions = { groupBy }
288+ expandToViewport = { true }
289+ disabled = { isLoading || isFetching }
290+ />
291+ </ div >
277292 </ div >
278- </ div >
279- )
280- }
281- />
293+ )
294+ }
295+ />
296+ < />
282297 ) ;
283298} ;
0 commit comments