Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/backend.ai-ui/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,8 @@ export {
} from './useBAILogger';
export type { LoggerPlugin, LogContext, BAILogger } from './useBAILogger';
export { useEventNotStable } from './useEventNotStable';
export { useProjectResourceGroups } from './useProjectResourceGroups';
export {
useProjectResourceGroups,
StorageHostFetchError,
} from './useProjectResourceGroups';
export type { ScalingGroupItem } from './useProjectResourceGroups';
69 changes: 52 additions & 17 deletions packages/backend.ai-ui/src/hooks/useProjectResourceGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ export interface ScalingGroupItem {
name: string;
}

/**
* Thrown when the vfolder host info fetch (`/folders/_/hosts`) inside
* `useProjectResourceGroups` fails. Tagging the failure lets callers wrap
* the hook with a dedicated error boundary that can distinguish this
* specific case from unrelated render errors and surface a targeted
* message (and discriminate it from the parallel scaling-groups fetch
* failure, which is re-thrown as-is so an outer boundary handles it).
*/
export class StorageHostFetchError extends Error {
readonly originalError: unknown;
constructor(originalError: unknown) {
super(
originalError instanceof Error
? originalError.message
: 'Failed to fetch storage host information.',
);
this.name = 'StorageHostFetchError';
this.originalError = originalError;
}
}

interface VolumeInfo {
backend: string;
capabilities: string[];
Expand All @@ -15,19 +36,20 @@ interface VolumeInfo {
sftp_scaling_groups?: string[];
}

interface ScalingGroupsResponse {
scaling_groups: ScalingGroupItem[];
}

interface StorageHostsResponse {
allowed: string[];
default: string;
volume_info: {
[key: string]: VolumeInfo;
};
}

type ProjectResourceGroupsQueryResult =
| [
{
scaling_groups: ScalingGroupItem[];
},
{
allowed: string[];
default: string;
volume_info: {
[key: string]: VolumeInfo;
};
},
]
| [ScalingGroupsResponse, StorageHostsResponse]
| null;

interface UseProjectResourceGroupsOptions {
Expand Down Expand Up @@ -60,24 +82,37 @@ export const useProjectResourceGroups = (

const { data } = useSuspenseTanQuery<ProjectResourceGroupsQueryResult>({
queryKey: ['ResourceGroupSelectQuery', projectName],
queryFn: () => {
queryFn: async () => {
// Short-circuit when there is no project context yet — avoids hitting
// `/scaling-groups?group=` and `/folders/_/hosts` with an unscoped query.
if (!projectName) {
return Promise.resolve(null);
return null;
}
const search = new URLSearchParams();
search.set('group', projectName);
return Promise.all([
// Run both fetches concurrently but discriminate failures: a host-info
// failure is tagged with `StorageHostFetchError` so a dedicated boundary
// can surface it, while a scaling-groups failure is re-thrown as-is and
// bubbles up to the generic error boundary. Host-info failure takes
// precedence when both fail because SFTP filtering depends on it and
// the result is otherwise unusable.
const [scalingGroupsResult, hostsResult] = await Promise.allSettled([
baiRequestWithPromise({
method: 'GET',
url: `/scaling-groups?${search.toString()}`,
}),
}) as Promise<ScalingGroupsResponse>,
baiRequestWithPromise({
method: 'GET',
url: `/folders/_/hosts`,
}),
}) as Promise<StorageHostsResponse>,
]);
if (hostsResult.status === 'rejected') {
throw new StorageHostFetchError(hostsResult.reason);
}
if (scalingGroupsResult.status === 'rejected') {
throw scalingGroupsResult.reason;
}
return [scalingGroupsResult.value, hostsResult.value];
},
staleTime: 1000 * 60 * 5, // Cache for 5 minutes
});
Expand Down
59 changes: 59 additions & 0 deletions react/src/components/StorageHostFetchErrorBoundary.tsx
Comment thread
ironAiken2 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
@license
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
*/
import { Button, Result } from 'antd';
import { BAIFlex, StorageHostFetchError } from 'backend.ai-ui';
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';

interface StorageHostFetchErrorBoundaryProps {
children: React.ReactNode;
style?: React.CSSProperties;
}

const StorageHostFetchErrorBoundary: React.FC<
StorageHostFetchErrorBoundaryProps
> = ({ children, style }) => {
'use memo';
const { t } = useTranslation();
return (
<ErrorBoundary
fallbackRender={({ error }) => {
if (!(error instanceof StorageHostFetchError)) {
// Re-throw non-storage errors so the outer BAIErrorBoundary handles them.
throw error;
}
return (
<BAIFlex
style={{ margin: 'auto', ...style }}
justify="center"
align="center"
>
<Result
status="warning"
title={t('errorBoundary.StorageHostFetchFailedTitle')}
extra={
<BAIFlex direction="column" gap="md">
<Button
type="primary"
onClick={() => {
globalThis.history.back();
}}
>
{t('button.GoBackToPreviousPage')}
</Button>
</BAIFlex>
}
/>
</BAIFlex>
);
}}
>
{children}
</ErrorBoundary>
);
};

export default StorageHostFetchErrorBoundary;
21 changes: 12 additions & 9 deletions react/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import LocationStateBreadCrumb from './components/LocationStateBreadCrumb';
import LoginView from './components/LoginView';
import MainLayout from './components/MainLayout/MainLayout';
import { STokenLoginBoundary } from './components/STokenLoginBoundary';
import StorageHostFetchErrorBoundary from './components/StorageHostFetchErrorBoundary';
import WebUINavigate from './components/WebUINavigate';
import { persistPostLoginState } from './helper/loginSessionAuth';
import { useSuspendedBackendaiClient } from './hooks';
Expand Down Expand Up @@ -258,15 +259,17 @@ export const mainLayoutChildRoutes: RouteObject[] = [
style={{ paddingBottom: token.paddingContentVerticalLG }}
>
<LocationStateBreadCrumb />
<Suspense
fallback={
<BAIFlex direction="column" style={{ maxWidth: 700 }}>
<Skeleton active />
</BAIFlex>
}
>
<SessionLauncherPage />
</Suspense>
<StorageHostFetchErrorBoundary>
<Suspense
fallback={
<BAIFlex direction="column" style={{ maxWidth: 700 }}>
<Skeleton active />
</BAIFlex>
}
>
<SessionLauncherPage />
</Suspense>
</StorageHostFetchErrorBoundary>
</BAIFlex>
);
},
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "Fertig",
"ForceTerminate": "Beenden erzwingen",
"Generate": "Generieren",
"GoBackToPreviousPage": "Zurück zur vorherigen Seite",
"GoBackToStartPage": "Zurück zur Seite {{title}}",
"Info": "Info",
"More": "Mehr",
Expand Down Expand Up @@ -1384,6 +1385,7 @@
"ReloadPage": "Laden Sie die Seite neu",
"ResetErrorBoundary": "ErrorBoundary zurücksetzen",
"StackTrace": "Stack-Trace:",
"StorageHostFetchFailedTitle": "Speicher-Host-Informationen konnten nicht abgerufen werden.",
"Title": "Es ist ein Fehler aufgetreten."
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "Φινίρισμα",
"ForceTerminate": "Δύναμη τερματισμού",
"Generate": "Παράγω",
"GoBackToPreviousPage": "Επιστροφή στην προηγούμενη σελίδα",
"GoBackToStartPage": "Επιστροφή στη σελίδα {{title}}",
"Info": "Πληροφορίες",
"More": "Περισσότερα",
Expand Down Expand Up @@ -1384,6 +1385,7 @@
"ReloadPage": "Επαναφόρτωση της σελίδας",
"ResetErrorBoundary": "Επαναφορά ErrorBoundary",
"StackTrace": "Ιχνη στοίβας:",
"StorageHostFetchFailedTitle": "Αποτυχία ανάκτησης πληροφοριών κεντρικού υπολογιστή αποθήκευσης.",
"Title": "Εμφανίστηκε σφάλμα."
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@
"Finish": "Finish",
"ForceTerminate": "Force Terminate",
"Generate": "Generate",
"GoBackToPreviousPage": "Go back to the previous page",
"GoBackToStartPage": "Go back to the {{title}} page",
"Info": "Info",
"More": "More",
Expand Down Expand Up @@ -1403,6 +1404,7 @@
"ReloadPage": "Reload the page",
"ResetErrorBoundary": "Reset ErrorBoundary",
"StackTrace": "Stack Trace:",
"StorageHostFetchFailedTitle": "Failed to fetch storage host information.",
"Title": "An error has occurred."
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "Acabado",
"ForceTerminate": "Forzar finalización",
"Generate": "Genere",
"GoBackToPreviousPage": "Volver a la página anterior",
"GoBackToStartPage": "Volver a la página {{title}}",
"Info": "Info",
"More": "Más",
Expand Down Expand Up @@ -1384,6 +1385,7 @@
"ReloadPage": "Recargar la página",
"ResetErrorBoundary": "Restablecer ErrorBoundary",
"StackTrace": "Traza de pila:",
"StorageHostFetchFailedTitle": "No se pudo obtener la información del host de almacenamiento.",
"Title": "Se ha producido un error."
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "Viimeistely",
"ForceTerminate": "Pakota lopettamaan",
"Generate": "Luo",
"GoBackToPreviousPage": "Palaa edelliselle sivulle",
"GoBackToStartPage": "Palaa sivulle {{title}}",
"Info": "Tiedot",
"More": "Lisää",
Expand Down Expand Up @@ -1384,6 +1385,7 @@
"ReloadPage": "Lataa sivu uudelleen",
"ResetErrorBoundary": "Nollaa ErrorBoundary",
"StackTrace": "Pinon jäljitys:",
"StorageHostFetchFailedTitle": "Tallennusisännän tietojen hakeminen epäonnistui.",
"Title": "On tapahtunut virhe."
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "Finir",
"ForceTerminate": "Forcer l'arrêt",
"Generate": "produire",
"GoBackToPreviousPage": "Retour à la page précédente",
"GoBackToStartPage": "Retour à la page {{title}}",
"Info": "Info",
"More": "Plus",
Expand Down Expand Up @@ -1385,6 +1386,7 @@
"ReloadPage": "Recharger la page",
"ResetErrorBoundary": "Réinitialisation de la limite d'erreur",
"StackTrace": "Trace de la pile :",
"StorageHostFetchFailedTitle": "Échec de la récupération des informations sur l'hôte de stockage.",
"Title": "Une erreur s'est produite."
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "Selesai",
"ForceTerminate": "Hentikan paksa",
"Generate": "Hasilkan",
"GoBackToPreviousPage": "Kembali ke halaman sebelumnya",
"GoBackToStartPage": "Kembali ke halaman {{title}}",
"Info": "Info",
"More": "Selengkapnya",
Expand Down Expand Up @@ -1385,6 +1386,7 @@
"ReloadPage": "Muat ulang halaman",
"ResetErrorBoundary": "Setel Ulang Batas Kesalahan",
"StackTrace": "Jejak tumpukan:",
"StorageHostFetchFailedTitle": "Gagal mengambil informasi host penyimpanan.",
"Title": "Telah terjadi kesalahan."
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "finire",
"ForceTerminate": "Termina forzata",
"Generate": "creare",
"GoBackToPreviousPage": "Torna alla pagina precedente",
"GoBackToStartPage": "Torna alla pagina {{title}}",
"Info": "Info",
"More": "Altro",
Expand Down Expand Up @@ -1384,6 +1385,7 @@
"ReloadPage": "Ricarica la pagina",
"ResetErrorBoundary": "Azzeramento di ErrorBoundary",
"StackTrace": "Traccia dello stack:",
"StorageHostFetchFailedTitle": "Impossibile recuperare le informazioni sull'host di archiviazione.",
"Title": "Si è verificato un errore."
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "終了",
"ForceTerminate": "強制終了",
"Generate": "生む",
"GoBackToPreviousPage": "前のページに戻る",
"GoBackToStartPage": "{{title}}ページに戻る",
"Info": "情報",
"More": "その他",
Expand Down Expand Up @@ -1384,6 +1385,7 @@
"ReloadPage": "ページの更新",
"ResetErrorBoundary": "ErrorBoundaryの初期化",
"StackTrace": "スタックトレース:",
"StorageHostFetchFailedTitle": "ストレージホスト情報の取得に失敗しました。",
"Title": "エラーが発生しました。"
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "완료",
"ForceTerminate": "강제 종료",
"Generate": "생성하기",
"GoBackToPreviousPage": "이전 페이지로 이동",
"GoBackToStartPage": "{{title}} 페이지로 돌아가기",
"Info": "정보",
"More": "더보기",
Expand Down Expand Up @@ -1385,6 +1386,7 @@
"ReloadPage": "페이지 새로고침",
"ResetErrorBoundary": "ErrorBoundary 초기화",
"StackTrace": "스택 트레이스:",
"StorageHostFetchFailedTitle": "스토리지 호스트 정보를 가져오는데 실패했습니다.",
"Title": "에러가 발생했습니다."
},
"explorer": {
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/mn.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
"Finish": "Дуусах",
"ForceTerminate": "Хүчээр цуцлах",
"Generate": "Үүсгэх",
"GoBackToPreviousPage": "Өмнөх хуудас руу буцах",
"GoBackToStartPage": "{{title}} хуудас руу буцах",
"Info": "Мэдээлэл",
"More": "Дэлгэрэнгүй",
Expand Down Expand Up @@ -1384,6 +1385,7 @@
"ReloadPage": "Хуудсыг дахин ачаална уу",
"ResetErrorBoundary": "Алдааны хил хязгаарыг дахин тохируулах",
"StackTrace": "Стэк мөр:",
"StorageHostFetchFailedTitle": "Хадгалалтын хостын мэдээллийг авах амжилтгүй боллоо.",
"Title": "Алдаа гарсан байна."
},
"explorer": {
Expand Down
Loading
Loading