@@ -6,6 +6,27 @@ export interface ScalingGroupItem {
66 name : string ;
77}
88
9+ /**
10+ * Thrown when the vfolder host info fetch (`/folders/_/hosts`) inside
11+ * `useProjectResourceGroups` fails. Tagging the failure lets callers wrap
12+ * the hook with a dedicated error boundary that can distinguish this
13+ * specific case from unrelated render errors and surface a targeted
14+ * message (and discriminate it from the parallel scaling-groups fetch
15+ * failure, which is re-thrown as-is so an outer boundary handles it).
16+ */
17+ export class StorageHostFetchError extends Error {
18+ readonly originalError : unknown ;
19+ constructor ( originalError : unknown ) {
20+ super (
21+ originalError instanceof Error
22+ ? originalError . message
23+ : 'Failed to fetch storage host information.' ,
24+ ) ;
25+ this . name = 'StorageHostFetchError' ;
26+ this . originalError = originalError ;
27+ }
28+ }
29+
930interface VolumeInfo {
1031 backend : string ;
1132 capabilities : string [ ] ;
@@ -15,19 +36,20 @@ interface VolumeInfo {
1536 sftp_scaling_groups ?: string [ ] ;
1637}
1738
39+ interface ScalingGroupsResponse {
40+ scaling_groups : ScalingGroupItem [ ] ;
41+ }
42+
43+ interface StorageHostsResponse {
44+ allowed : string [ ] ;
45+ default : string ;
46+ volume_info : {
47+ [ key : string ] : VolumeInfo ;
48+ } ;
49+ }
50+
1851type ProjectResourceGroupsQueryResult =
19- | [
20- {
21- scaling_groups : ScalingGroupItem [ ] ;
22- } ,
23- {
24- allowed : string [ ] ;
25- default : string ;
26- volume_info : {
27- [ key : string ] : VolumeInfo ;
28- } ;
29- } ,
30- ]
52+ | [ ScalingGroupsResponse , StorageHostsResponse ]
3153 | null ;
3254
3355interface UseProjectResourceGroupsOptions {
@@ -60,24 +82,37 @@ export const useProjectResourceGroups = (
6082
6183 const { data } = useSuspenseTanQuery < ProjectResourceGroupsQueryResult > ( {
6284 queryKey : [ 'ResourceGroupSelectQuery' , projectName ] ,
63- queryFn : ( ) => {
85+ queryFn : async ( ) => {
6486 // Short-circuit when there is no project context yet — avoids hitting
6587 // `/scaling-groups?group=` and `/folders/_/hosts` with an unscoped query.
6688 if ( ! projectName ) {
67- return Promise . resolve ( null ) ;
89+ return null ;
6890 }
6991 const search = new URLSearchParams ( ) ;
7092 search . set ( 'group' , projectName ) ;
71- return Promise . all ( [
93+ // Run both fetches concurrently but discriminate failures: a host-info
94+ // failure is tagged with `StorageHostFetchError` so a dedicated boundary
95+ // can surface it, while a scaling-groups failure is re-thrown as-is and
96+ // bubbles up to the generic error boundary. Host-info failure takes
97+ // precedence when both fail because SFTP filtering depends on it and
98+ // the result is otherwise unusable.
99+ const [ scalingGroupsResult , hostsResult ] = await Promise . allSettled ( [
72100 baiRequestWithPromise ( {
73101 method : 'GET' ,
74102 url : `/scaling-groups?${ search . toString ( ) } ` ,
75- } ) ,
103+ } ) as Promise < ScalingGroupsResponse > ,
76104 baiRequestWithPromise ( {
77105 method : 'GET' ,
78106 url : `/folders/_/hosts` ,
79- } ) ,
107+ } ) as Promise < StorageHostsResponse > ,
80108 ] ) ;
109+ if ( hostsResult . status === 'rejected' ) {
110+ throw new StorageHostFetchError ( hostsResult . reason ) ;
111+ }
112+ if ( scalingGroupsResult . status === 'rejected' ) {
113+ throw scalingGroupsResult . reason ;
114+ }
115+ return [ scalingGroupsResult . value , hostsResult . value ] ;
81116 } ,
82117 staleTime : 1000 * 60 * 5 , // Cache for 5 minutes
83118 } ) ;
0 commit comments