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
50 changes: 45 additions & 5 deletions packages/backend.ai-ui/src/hooks/useErrorMessageResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,49 @@ export type ESMClientErrorResponse = {
response?: ErrorResponse;
};

type ErrorLike = Partial<ErrorResponse & Error & ESMClientErrorResponse>;

export type GetErrorMessageOptions = {
defaultMessage?: string;
/**
* 'normal' (default) returns just the human-readable message, appending
* `(error_code)` when present — suitable for end-user surfaces.
*
* 'detail' additionally appends the HTTP `statusCode` and `error_code`
* in the existing parenthesized suffix style, e.g.
* `message text (HTTP 500, BAI_E0001)`. Use this on operator-facing
* failure surfaces (e.g. SFTP session creation) where classifying the
* failure (4xx policy/quota vs 5xx agent/storage) matters more than a
* clean copy.
*/
verbosity?: 'normal' | 'detail';
};

const useErrorMessageResolver = () => {
const { t } = useTranslation();

const isErrorLike = (
error: unknown,
): error is Partial<ErrorResponse & Error> => {
const isErrorLike = (error: unknown): error is ErrorLike => {
return typeof error === 'object' && error !== null;
};

/**
* Resolves the error message for a given error object.
* @param error - The error object to resolve.
* @param defaultMessage - (optional) The default message to return if no specific message is found.
* @param defaultMessageOrOptions - Either a default fallback string
* (backwards compatible) or an options object with `defaultMessage`
* and `verbosity`.
* @returns - The resolved error message (string).
*/
const getErrorMessage = (error: unknown, defaultMessage?: string): string => {
const getErrorMessage = (
error: unknown,
defaultMessageOrOptions?: string | GetErrorMessageOptions,
): string => {
const options: GetErrorMessageOptions =
typeof defaultMessageOrOptions === 'string'
? { defaultMessage: defaultMessageOrOptions }
: (defaultMessageOrOptions ?? {});
const { defaultMessage, verbosity = 'normal' } = options;

let errorMsg = defaultMessage || t('error.UnknownError');
if (!error || !isErrorLike(error)) return errorMsg;

Expand All @@ -54,6 +81,19 @@ const useErrorMessageResolver = () => {
} else if (error.title) {
errorMsg = error.title;
}

if (verbosity === 'detail') {
// Suffix style — aligns with the existing `(error_code)` idiom.
// e.g. `<message> (HTTP 500, BAI_E0001)`.
const suffixParts = _.compact([
_.isNumber(error.statusCode) ? `HTTP ${error.statusCode}` : null,
error.error_code || null,
]);
return suffixParts.length > 0
? `${errorMsg} (${suffixParts.join(', ')})`
: errorMsg;
}

if (error.error_code) {
errorMsg = _.join([errorMsg, `(${error.error_code})`], ' ');
}
Expand Down
19 changes: 15 additions & 4 deletions react/src/components/SFTPServerButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import {
StartSessionWithDefaultValue,
useStartSession,
} from '../hooks/useStartSession';
import { openSFTPFailureModal } from './sftpFailureModal';
import { EllipsisOutlined } from '@ant-design/icons';
import { App, Dropdown, Image, Space, Tooltip } from 'antd';
import { App, Dropdown, Image, Space, Tooltip, theme } from 'antd';
import {
BAIButton,
BAIButtonProps,
Expand All @@ -46,6 +47,7 @@ const SFTPServerButton: React.FC<SFTPServerButtonProps> = ({
const { logger } = useBAILogger();
const { t } = useTranslation();
const { message, modal } = App.useApp();
const { token } = theme.useToken();

const webuiNavigate = useWebUINavigate();

Expand Down Expand Up @@ -159,9 +161,18 @@ const SFTPServerButton: React.FC<SFTPServerButtonProps> = ({
}
if (results?.rejected && results.rejected.length > 0) {
const error = results.rejected[0].reason;
modal.error({
title: error?.title,
content: getErrorMessage(error),
openSFTPFailureModal({
modal,
t,
token,
error,
getErrorMessage,
onGoToUploadSessions: () => {
webuiNavigate({
pathname: '/session',
search: '?type=system',
});
},
});
}
})
Expand Down
19 changes: 15 additions & 4 deletions react/src/components/SFTPServerButtonV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import {
StartSessionWithDefaultValue,
useStartSession,
} from '../hooks/useStartSession';
import { openSFTPFailureModal } from './sftpFailureModal';
import { EllipsisOutlined } from '@ant-design/icons';
import { App, Dropdown, Image, Space, Tooltip } from 'antd';
import { App, Dropdown, Image, Space, Tooltip, theme } from 'antd';
import {
BAIButton,
BAIButtonProps,
Expand All @@ -46,6 +47,7 @@ const SFTPServerButtonV2: React.FC<SFTPServerButtonV2Props> = ({
const { logger } = useBAILogger();
const { t } = useTranslation();
const { message, modal } = App.useApp();
const { token } = theme.useToken();

const webuiNavigate = useWebUINavigate();

Expand Down Expand Up @@ -159,9 +161,18 @@ const SFTPServerButtonV2: React.FC<SFTPServerButtonV2Props> = ({
}
if (results?.rejected && results.rejected.length > 0) {
const error = results.rejected[0].reason;
modal.error({
title: error?.title,
content: getErrorMessage(error),
openSFTPFailureModal({
modal,
t,
token,
error,
getErrorMessage,
onGoToUploadSessions: () => {
webuiNavigate({
pathname: '/session',
search: '?type=system',
});
},
});
}
})
Expand Down
61 changes: 61 additions & 0 deletions react/src/components/sftpFailureModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
@license
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
*/
import { App, GlobalToken, Typography } from 'antd';
import { useErrorMessageResolver } from 'backend.ai-ui';
import { TFunction } from 'i18next';

type ModalInstance = ReturnType<typeof App.useApp>['modal'];
type GetErrorMessage = ReturnType<
typeof useErrorMessageResolver
>['getErrorMessage'];

interface OpenSFTPFailureModalArgs {
modal: ModalInstance;
t: TFunction;
token: GlobalToken;
error: unknown;
getErrorMessage: GetErrorMessage;
onGoToUploadSessions: () => void;
}

/**
* Opens the SFTP session creation failure modal.
*
* Shared by `SFTPServerButton` and `SFTPServerButtonV2` (the legacy and
* Relay-node variants) so that copy, classification, and the recovery CTA
* stay in sync. Uses `modal.error` with `okCancel` to keep the native
* error styling (icon + colors) while still exposing a primary action
* that deep-links to the upload session list — the failure shown here is
* typically downstream of stale upload sessions piling up.
*/
export const openSFTPFailureModal = ({
modal,
t,
token,
error,
getErrorMessage,
onGoToUploadSessions,
}: OpenSFTPFailureModalArgs) => {
modal.error({
okCancel: true,
title: t('data.explorer.SFTPSessionCreationFailed'),
content: (
<Typography.Paragraph
style={{ marginTop: token.marginSM, marginBottom: 0 }}
>
<Typography.Text>
{getErrorMessage(error, { verbosity: 'detail' })}
</Typography.Text>
<br />
<Typography.Text type="secondary">
{t('data.explorer.SFTPSessionFailureHint')}
</Typography.Text>
</Typography.Paragraph>
),
okText: t('session.GoToUploadSessionList'),
cancelText: t('button.Close'),
onOk: onGoToUploadSessions,
});
};
6 changes: 3 additions & 3 deletions resources/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -701,8 +701,6 @@
"NoSharedFolders": "Keine freigegebenen Ordner",
"NoSharedUsers": "Niemand wurde eingeladen",
"NotEnoughResourceForFileBrowserSession": "Nicht genügend Ressourcen (CPU: 1 Core, Speicher: 0,5 GB), um die Sitzung für den Dateibrowser zu erstellen. Bitte überprüfen Sie die verfügbaren Ressourcen.",
"NumberOfSFTPSessionsExceededBody": "Sie führen alle verfügbaren Upload-Sitzungen aus, die Sie erstellen können. Bitte beenden Sie ungenutzte Upload-Sitzungen, bevor Sie eine neue Sitzung starten.",
"NumberOfSFTPSessionsExceededTitle": "Limit der laufenden Upload-Session erreicht",
"People": "Menschen",
"Permission": "Genehmigung",
"Permissions": "Berechtigungen",
Expand All @@ -713,7 +711,8 @@
"RenameAFolder": "Ordner umbenennen",
"RetryingOperation": "Wiederholung, um eine Sitzung festzulegen ...",
"RunSSH/SFTPserver": "SFTP-Server starten",
"SFTPSessionNotAvailable": "SFTP Session ist jetzt nicht verfügbar",
"SFTPSessionCreationFailed": "SFTP-Sitzung konnte nicht erstellt werden",
"SFTPSessionFailureHint": "Wenn noch viele Upload-Sitzungen aktiv sind, können durch das Schließen nicht genutzter Sitzungen Ressourcen auf dem SFTP-Host freigegeben werden.",
"ShareFolder": "Ordner teilen",
"Size": "Größe",
"StartingSSH/SFTPSession": "SFTP-Sitzung starten...",
Expand Down Expand Up @@ -2545,6 +2544,7 @@
"GPU": "GPU",
"GPU(MEM)": "GPU (Speicher)",
"Gaudi2Enabled": "Gaudi 2 NPU Ermöglicht",
"GoToUploadSessionList": "Zu Upload-Sitzungen wechseln",
"GracePeriod": "Karenzzeit",
"GracePeriodDesc": "Nach dieser anfänglichen Karenzzeit wird die Auslastungsprüfung aktiviert. Während dieser Zeit werden die Sitzungen nicht beendet, auch wenn die Auslastung niedrig ist.",
"Host": "Gastgeber",
Expand Down
6 changes: 3 additions & 3 deletions resources/i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -701,8 +701,6 @@
"NoSharedFolders": "Δεν υπάρχουν κοινόχρηστοι φάκελοι",
"NoSharedUsers": "Κανείς δεν έχει προσκληθεί",
"NotEnoughResourceForFileBrowserSession": "Δεν υπάρχουν αρκετοί πόροι (cpu: 1 Core, mem: 0,5 GB) για τη δημιουργία της περιόδου λειτουργίας για το πρόγραμμα περιήγησης αρχείων παρακαλούμε ελέγξτε τους διαθέσιμους πόρους.",
"NumberOfSFTPSessionsExceededBody": "Εκτελείτε όλες τις διαθέσιμες συνεδρίες μεταφόρτωσης που επιτρέπεται να δημιουργήσετε. Παρακαλούμε τερματίστε τις αχρησιμοποίητες συνεδρίες μεταφόρτωσης πριν ξεκινήσετε μια νέα συνεδρία.",
"NumberOfSFTPSessionsExceededTitle": "Έφθασε το όριο του αριθμού των τρεχουσών συνόδων μεταφόρτωσης",
"People": "Ανθρωποι",
"Permission": "Αδεια",
"Permissions": "Άδειες",
Expand All @@ -713,7 +711,8 @@
"RenameAFolder": "Μετονομασία φακέλου",
"RetryingOperation": "Επανένταξη για την καθιέρωση της συνεδρίας ...",
"RunSSH/SFTPserver": "Εκτέλεση διακομιστή SFTP",
"SFTPSessionNotAvailable": "Το SFTP Session δεν είναι διαθέσιμο τώρα",
"SFTPSessionCreationFailed": "Αποτυχία δημιουργίας συνεδρίας SFTP",
"SFTPSessionFailureHint": "Αν εκτελούνται ακόμα πολλές συνεδρίες μεταφόρτωσης, το κλείσιμο αχρησιμοποίητων μπορεί να ελευθερώσει πόρους στον κεντρικό υπολογιστή SFTP.",
"ShareFolder": "Κοινόχρηστο φάκελο",
"Size": "Μέγεθος",
"StartingSSH/SFTPSession": "Έναρξη συνεδρίας SFTP...",
Expand Down Expand Up @@ -2543,6 +2542,7 @@
"GPU": "GPU",
"GPU(MEM)": "GPU (μνήμη)",
"Gaudi2Enabled": "Gaudi 2 NPU Ενεργοποιημένο",
"GoToUploadSessionList": "Μετάβαση στις συνεδρίες μεταφόρτωσης",
"GracePeriod": "Περίοδος χάριτος",
"GracePeriodDesc": "Ο ελεγκτής αδράνειας χρήσης θα ενεργοποιηθεί μετά από αυτόν τον αρχικό χρόνο χάριτος. Κατά τη διάρκεια αυτού του χρόνου, οι σύνοδοι δεν τερματίζονται ακόμη και αν η χρησιμοποίηση είναι χαμηλή.",
"Host": "Πλήθος",
Expand Down
6 changes: 3 additions & 3 deletions resources/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,6 @@
"NoSharedFolders": "No shared folders",
"NoSharedUsers": "No one has been invited",
"NotEnoughResourceForFileBrowserSession": "No enough resources(cpu: 1 Core, mem: 0.5GB) to create the session for filebrowser. please check the available resources.",
"NumberOfSFTPSessionsExceededBody": "You are running all available upload sessions you are allowed to create. Please terminated unused upload sessions before starting a new session.",
"NumberOfSFTPSessionsExceededTitle": "Reached limit of running upload session count",
"People": "People",
"Permission": "Permission",
"Permissions": "Permissions",
Expand All @@ -731,7 +729,8 @@
"RenameAFolder": "Rename Folder",
"RetryingOperation": "Retrying to establish session...",
"RunSSH/SFTPserver": "Run SFTP server",
"SFTPSessionNotAvailable": "SFTP Session is not available now",
"SFTPSessionCreationFailed": "Failed to create SFTP session",
"SFTPSessionFailureHint": "If many upload sessions are still running, closing unused ones may free up resources on the SFTP host.",
"ShareFolder": "Share Folder",
"Size": "Size",
"StartingSSH/SFTPSession": "Starting SFTP session...",
Expand Down Expand Up @@ -2571,6 +2570,7 @@
"GPU": "GPU",
"GPU(MEM)": "GPU(Memory)",
"Gaudi2Enabled": "Gaudi 2 NPU Enabled",
"GoToUploadSessionList": "Go to upload sessions",
"GracePeriod": "Grace Period",
"GracePeriodDesc": "Utilization idle checker will be activated after this initial grace time. During this time, sessions are not terminated even if utilization is low.",
"Host": "Host",
Expand Down
6 changes: 3 additions & 3 deletions resources/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -701,8 +701,6 @@
"NoSharedFolders": "Sin carpetas compartidas",
"NoSharedUsers": "No se ha invitado a nadie.",
"NotEnoughResourceForFileBrowserSession": "No hay recursos suficientes (cpu: 1 Core, mem: 0.5GB) para crear la sesión para filebrowser. por favor compruebe los recursos disponibles.",
"NumberOfSFTPSessionsExceededBody": "Está ejecutando todas las sesiones de carga disponibles que puede crear. Elimine las sesiones de carga no utilizadas antes de iniciar una nueva sesión.",
"NumberOfSFTPSessionsExceededTitle": "Se ha alcanzado el límite del recuento de sesiones de carga en ejecución",
"People": "Personas",
"Permission": "Permiso",
"Permissions": "Permisos",
Expand All @@ -713,7 +711,8 @@
"RenameAFolder": "Renombrar carpeta",
"RetryingOperation": "Vuelva a intentar establecer sesión ...",
"RunSSH/SFTPserver": "Ejecutar servidor SFTP",
"SFTPSessionNotAvailable": "SFTP Session no está disponible ahora",
"SFTPSessionCreationFailed": "Error al crear la sesión SFTP",
"SFTPSessionFailureHint": "Si aún hay muchas sesiones de carga en ejecución, cerrar las que no se usan puede liberar recursos en el host SFTP.",
"ShareFolder": "Compartir carpeta",
"Size": "Talla",
"StartingSSH/SFTPSession": "Iniciando sesión SFTP...",
Expand Down Expand Up @@ -2543,6 +2542,7 @@
"GPU": "GPU",
"GPU(MEM)": "GPU (memoria)",
"Gaudi2Enabled": "Gaudi 2 NPU Activado",
"GoToUploadSessionList": "Ir a sesiones de carga",
"GracePeriod": "Período de gracia",
"GracePeriodDesc": "El comprobador de inactividad por utilización se activará después de este tiempo de gracia inicial. Durante este tiempo, las sesiones no se terminan aunque la utilización sea baja.",
"Host": "Anfitrión",
Expand Down
6 changes: 3 additions & 3 deletions resources/i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -701,8 +701,6 @@
"NoSharedFolders": "Ei jaettuja kansioita",
"NoSharedUsers": "Ketään ei ole kutsuttu.",
"NotEnoughResourceForFileBrowserSession": "Resurssit eivät riitä (cpu: 1 Core, mem: 0.5GB) filebrowser-istunnon luomiseen. tarkista käytettävissä olevat resurssit.",
"NumberOfSFTPSessionsExceededBody": "Käytössäsi ovat kaikki käytettävissä olevat latausistunnot, joita voit luoda. Lopeta käyttämättömät latausistunnot ennen uuden istunnon aloittamista.",
"NumberOfSFTPSessionsExceededTitle": "Käynnissä olevan latausistunnon lukumäärän raja saavutettu",
"People": "Ihmiset",
"Permission": "Lupa",
"Permissions": "Luvat",
Expand All @@ -713,7 +711,8 @@
"RenameAFolder": "Nimeä kansio uudelleen",
"RetryingOperation": "Uudelleenjärjestelmä istunnon perustamiseksi ...",
"RunSSH/SFTPserver": "Käynnistä SFTP-palvelin",
"SFTPSessionNotAvailable": "SFTP-istunto ei ole nyt käytettävissä",
"SFTPSessionCreationFailed": "SFTP-istunnon luominen epäonnistui",
"SFTPSessionFailureHint": "Jos monia lataamisistuntoja on yhä käynnissä, käyttämättömien sulkeminen voi vapauttaa resursseja SFTP-palvelimella.",
"ShareFolder": "Jaa kansio",
"Size": "Koko",
"StartingSSH/SFTPSession": "SFTP-istunnon käynnistäminen...",
Expand Down Expand Up @@ -2543,6 +2542,7 @@
"GPU": "GPU",
"GPU(MEM)": "GPU (muisti)",
"Gaudi2Enabled": "Gaudi 2 NPU Käytössä",
"GoToUploadSessionList": "Siirry lataamisistuntoihin",
"GracePeriod": "Karenssiaika",
"GracePeriodDesc": "Käyttötarkkailu aktivoituu tämän alkuvapauden jälkeen. Tänä aikana istuntoja ei lopeteta, vaikka käyttöaste olisi alhainen.",
"Host": "Isäntä",
Expand Down
6 changes: 3 additions & 3 deletions resources/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -701,8 +701,6 @@
"NoSharedFolders": "Aucun dossier partagé",
"NoSharedUsers": "Personne n'a été invité.",
"NotEnoughResourceForFileBrowserSession": "Pas assez de ressources (cpu : 1 Core, mem : 0,5 Go) pour créer la session pour le navigateur de fichiers. veuillez vérifier les ressources disponibles.",
"NumberOfSFTPSessionsExceededBody": "Vous exécutez toutes les sessions de téléchargement que vous êtes autorisé à créer. Veuillez mettre fin aux sessions de téléchargement inutilisées avant de commencer une nouvelle session.",
"NumberOfSFTPSessionsExceededTitle": "Limite atteinte pour le nombre de sessions de téléchargement en cours",
"People": "Gens",
"Permission": "Autorisation",
"Permissions": "Autorisations",
Expand All @@ -713,7 +711,8 @@
"RenameAFolder": "Renommer le dossier",
"RetryingOperation": "Réessayant pour établir la session ...",
"RunSSH/SFTPserver": "Exécuter le serveur SFTP",
"SFTPSessionNotAvailable": "SFTP Session n'est pas disponible pour le moment",
"SFTPSessionCreationFailed": "Échec de la création de la session SFTP",
"SFTPSessionFailureHint": "Si de nombreuses sessions d'envoi sont encore en cours, la fermeture de celles inutilisées peut libérer des ressources sur l'hôte SFTP.",
"ShareFolder": "Dossier de partage",
"Size": "Taille",
"StartingSSH/SFTPSession": "Démarrage de la session SFTP...",
Expand Down Expand Up @@ -2545,6 +2544,7 @@
"GPU": "GPU",
"GPU(MEM)": "GPU (mémoire)",
"Gaudi2Enabled": "Gaudi 2 NPU Activé",
"GoToUploadSessionList": "Aller aux sessions d'envoi",
"GracePeriod": "Délai de grâce",
"GracePeriodDesc": "Le vérificateur d'inactivité d'utilisation sera activé après ce temps de grâce initial. Pendant cette période, les sessions ne sont pas interrompues même si l'utilisation est faible.",
"Host": "Hôte",
Expand Down
Loading
Loading