Skip to content

Commit f189f8f

Browse files
committed
feat(FR-2898): improve SFTP session creation error feedback
1 parent f4e2e10 commit f189f8f

25 files changed

Lines changed: 199 additions & 76 deletions

packages/backend.ai-ui/src/hooks/useErrorMessageResolver.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,49 @@ export type ESMClientErrorResponse = {
2626
response?: ErrorResponse;
2727
};
2828

29+
type ErrorLike = Partial<ErrorResponse & Error & ESMClientErrorResponse>;
30+
31+
export type GetErrorMessageOptions = {
32+
defaultMessage?: string;
33+
/**
34+
* 'normal' (default) returns just the human-readable message, appending
35+
* `(error_code)` when present — suitable for end-user surfaces.
36+
*
37+
* 'detail' additionally appends the HTTP `statusCode` and `error_code`
38+
* in the existing parenthesized suffix style, e.g.
39+
* `message text (HTTP 500, BAI_E0001)`. Use this on operator-facing
40+
* failure surfaces (e.g. SFTP session creation) where classifying the
41+
* failure (4xx policy/quota vs 5xx agent/storage) matters more than a
42+
* clean copy.
43+
*/
44+
verbosity?: 'normal' | 'detail';
45+
};
46+
2947
const useErrorMessageResolver = () => {
3048
const { t } = useTranslation();
3149

32-
const isErrorLike = (
33-
error: unknown,
34-
): error is Partial<ErrorResponse & Error> => {
50+
const isErrorLike = (error: unknown): error is ErrorLike => {
3551
return typeof error === 'object' && error !== null;
3652
};
3753

3854
/**
3955
* Resolves the error message for a given error object.
4056
* @param error - The error object to resolve.
41-
* @param defaultMessage - (optional) The default message to return if no specific message is found.
57+
* @param defaultMessageOrOptions - Either a default fallback string
58+
* (backwards compatible) or an options object with `defaultMessage`
59+
* and `verbosity`.
4260
* @returns - The resolved error message (string).
4361
*/
44-
const getErrorMessage = (error: unknown, defaultMessage?: string): string => {
62+
const getErrorMessage = (
63+
error: unknown,
64+
defaultMessageOrOptions?: string | GetErrorMessageOptions,
65+
): string => {
66+
const options: GetErrorMessageOptions =
67+
typeof defaultMessageOrOptions === 'string'
68+
? { defaultMessage: defaultMessageOrOptions }
69+
: (defaultMessageOrOptions ?? {});
70+
const { defaultMessage, verbosity = 'normal' } = options;
71+
4572
let errorMsg = defaultMessage || t('error.UnknownError');
4673
if (!error || !isErrorLike(error)) return errorMsg;
4774

@@ -54,6 +81,19 @@ const useErrorMessageResolver = () => {
5481
} else if (error.title) {
5582
errorMsg = error.title;
5683
}
84+
85+
if (verbosity === 'detail') {
86+
// Suffix style — aligns with the existing `(error_code)` idiom.
87+
// e.g. `<message> (HTTP 500, BAI_E0001)`.
88+
const suffixParts = _.compact([
89+
_.isNumber(error.statusCode) ? `HTTP ${error.statusCode}` : null,
90+
error.error_code || null,
91+
]);
92+
return suffixParts.length > 0
93+
? `${errorMsg} (${suffixParts.join(', ')})`
94+
: errorMsg;
95+
}
96+
5797
if (error.error_code) {
5898
errorMsg = _.join([errorMsg, `(${error.error_code})`], ' ');
5999
}

react/src/components/SFTPServerButton.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import {
1818
StartSessionWithDefaultValue,
1919
useStartSession,
2020
} from '../hooks/useStartSession';
21+
import { openSFTPFailureModal } from './sftpFailureModal';
2122
import { EllipsisOutlined } from '@ant-design/icons';
22-
import { App, Dropdown, Image, Space, Tooltip } from 'antd';
23+
import { App, Dropdown, Image, Space, Tooltip, theme } from 'antd';
2324
import {
2425
BAIButton,
2526
BAIButtonProps,
@@ -46,6 +47,7 @@ const SFTPServerButton: React.FC<SFTPServerButtonProps> = ({
4647
const { logger } = useBAILogger();
4748
const { t } = useTranslation();
4849
const { message, modal } = App.useApp();
50+
const { token } = theme.useToken();
4951

5052
const webuiNavigate = useWebUINavigate();
5153

@@ -159,9 +161,18 @@ const SFTPServerButton: React.FC<SFTPServerButtonProps> = ({
159161
}
160162
if (results?.rejected && results.rejected.length > 0) {
161163
const error = results.rejected[0].reason;
162-
modal.error({
163-
title: error?.title,
164-
content: getErrorMessage(error),
164+
openSFTPFailureModal({
165+
modal,
166+
t,
167+
token,
168+
error,
169+
getErrorMessage,
170+
onGoToUploadSessions: () => {
171+
webuiNavigate({
172+
pathname: '/session',
173+
search: '?type=system',
174+
});
175+
},
165176
});
166177
}
167178
})

react/src/components/SFTPServerButtonV2.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import {
1818
StartSessionWithDefaultValue,
1919
useStartSession,
2020
} from '../hooks/useStartSession';
21+
import { openSFTPFailureModal } from './sftpFailureModal';
2122
import { EllipsisOutlined } from '@ant-design/icons';
22-
import { App, Dropdown, Image, Space, Tooltip } from 'antd';
23+
import { App, Dropdown, Image, Space, Tooltip, theme } from 'antd';
2324
import {
2425
BAIButton,
2526
BAIButtonProps,
@@ -46,6 +47,7 @@ const SFTPServerButtonV2: React.FC<SFTPServerButtonV2Props> = ({
4647
const { logger } = useBAILogger();
4748
const { t } = useTranslation();
4849
const { message, modal } = App.useApp();
50+
const { token } = theme.useToken();
4951

5052
const webuiNavigate = useWebUINavigate();
5153

@@ -159,9 +161,18 @@ const SFTPServerButtonV2: React.FC<SFTPServerButtonV2Props> = ({
159161
}
160162
if (results?.rejected && results.rejected.length > 0) {
161163
const error = results.rejected[0].reason;
162-
modal.error({
163-
title: error?.title,
164-
content: getErrorMessage(error),
164+
openSFTPFailureModal({
165+
modal,
166+
t,
167+
token,
168+
error,
169+
getErrorMessage,
170+
onGoToUploadSessions: () => {
171+
webuiNavigate({
172+
pathname: '/session',
173+
search: '?type=system',
174+
});
175+
},
165176
});
166177
}
167178
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
@license
3+
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
4+
*/
5+
import { App, GlobalToken, Typography } from 'antd';
6+
import { useErrorMessageResolver } from 'backend.ai-ui';
7+
import { TFunction } from 'i18next';
8+
9+
type ModalInstance = ReturnType<typeof App.useApp>['modal'];
10+
type GetErrorMessage = ReturnType<
11+
typeof useErrorMessageResolver
12+
>['getErrorMessage'];
13+
14+
interface OpenSFTPFailureModalArgs {
15+
modal: ModalInstance;
16+
t: TFunction;
17+
token: GlobalToken;
18+
error: unknown;
19+
getErrorMessage: GetErrorMessage;
20+
onGoToUploadSessions: () => void;
21+
}
22+
23+
/**
24+
* Opens the SFTP session creation failure modal.
25+
*
26+
* Shared by `SFTPServerButton` and `SFTPServerButtonV2` (the legacy and
27+
* Relay-node variants) so that copy, classification, and the recovery CTA
28+
* stay in sync. Uses `modal.error` with `okCancel` to keep the native
29+
* error styling (icon + colors) while still exposing a primary action
30+
* that deep-links to the upload session list — the failure shown here is
31+
* typically downstream of stale upload sessions piling up.
32+
*/
33+
export const openSFTPFailureModal = ({
34+
modal,
35+
t,
36+
token,
37+
error,
38+
getErrorMessage,
39+
onGoToUploadSessions,
40+
}: OpenSFTPFailureModalArgs) => {
41+
modal.error({
42+
okCancel: true,
43+
title: t('data.explorer.SFTPSessionCreationFailed'),
44+
content: (
45+
<Typography.Paragraph
46+
style={{ marginTop: token.marginSM, marginBottom: 0 }}
47+
>
48+
<Typography.Text>
49+
{getErrorMessage(error, { verbosity: 'detail' })}
50+
</Typography.Text>
51+
<br />
52+
<Typography.Text type="secondary">
53+
{t('data.explorer.SFTPSessionFailureHint')}
54+
</Typography.Text>
55+
</Typography.Paragraph>
56+
),
57+
okText: t('session.GoToUploadSessionList'),
58+
cancelText: t('button.Close'),
59+
onOk: onGoToUploadSessions,
60+
});
61+
};

resources/i18n/de.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -701,8 +701,6 @@
701701
"NoSharedFolders": "Keine freigegebenen Ordner",
702702
"NoSharedUsers": "Niemand wurde eingeladen",
703703
"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.",
704-
"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.",
705-
"NumberOfSFTPSessionsExceededTitle": "Limit der laufenden Upload-Session erreicht",
706704
"People": "Menschen",
707705
"Permission": "Genehmigung",
708706
"Permissions": "Berechtigungen",
@@ -713,7 +711,8 @@
713711
"RenameAFolder": "Ordner umbenennen",
714712
"RetryingOperation": "Wiederholung, um eine Sitzung festzulegen ...",
715713
"RunSSH/SFTPserver": "SFTP-Server starten",
716-
"SFTPSessionNotAvailable": "SFTP Session ist jetzt nicht verfügbar",
714+
"SFTPSessionCreationFailed": "SFTP-Sitzung konnte nicht erstellt werden",
715+
"SFTPSessionFailureHint": "Wenn noch viele Upload-Sitzungen aktiv sind, können durch das Schließen nicht genutzter Sitzungen Ressourcen auf dem SFTP-Host freigegeben werden.",
717716
"ShareFolder": "Ordner teilen",
718717
"Size": "Größe",
719718
"StartingSSH/SFTPSession": "SFTP-Sitzung starten...",
@@ -2545,6 +2544,7 @@
25452544
"GPU": "GPU",
25462545
"GPU(MEM)": "GPU (Speicher)",
25472546
"Gaudi2Enabled": "Gaudi 2 NPU Ermöglicht",
2547+
"GoToUploadSessionList": "Zu Upload-Sitzungen wechseln",
25482548
"GracePeriod": "Karenzzeit",
25492549
"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.",
25502550
"Host": "Gastgeber",

resources/i18n/el.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -701,8 +701,6 @@
701701
"NoSharedFolders": "Δεν υπάρχουν κοινόχρηστοι φάκελοι",
702702
"NoSharedUsers": "Κανείς δεν έχει προσκληθεί",
703703
"NotEnoughResourceForFileBrowserSession": "Δεν υπάρχουν αρκετοί πόροι (cpu: 1 Core, mem: 0,5 GB) για τη δημιουργία της περιόδου λειτουργίας για το πρόγραμμα περιήγησης αρχείων παρακαλούμε ελέγξτε τους διαθέσιμους πόρους.",
704-
"NumberOfSFTPSessionsExceededBody": "Εκτελείτε όλες τις διαθέσιμες συνεδρίες μεταφόρτωσης που επιτρέπεται να δημιουργήσετε. Παρακαλούμε τερματίστε τις αχρησιμοποίητες συνεδρίες μεταφόρτωσης πριν ξεκινήσετε μια νέα συνεδρία.",
705-
"NumberOfSFTPSessionsExceededTitle": "Έφθασε το όριο του αριθμού των τρεχουσών συνόδων μεταφόρτωσης",
706704
"People": "Ανθρωποι",
707705
"Permission": "Αδεια",
708706
"Permissions": "Άδειες",
@@ -713,7 +711,8 @@
713711
"RenameAFolder": "Μετονομασία φακέλου",
714712
"RetryingOperation": "Επανένταξη για την καθιέρωση της συνεδρίας ...",
715713
"RunSSH/SFTPserver": "Εκτέλεση διακομιστή SFTP",
716-
"SFTPSessionNotAvailable": "Το SFTP Session δεν είναι διαθέσιμο τώρα",
714+
"SFTPSessionCreationFailed": "Αποτυχία δημιουργίας συνεδρίας SFTP",
715+
"SFTPSessionFailureHint": "Αν εκτελούνται ακόμα πολλές συνεδρίες μεταφόρτωσης, το κλείσιμο αχρησιμοποίητων μπορεί να ελευθερώσει πόρους στον κεντρικό υπολογιστή SFTP.",
717716
"ShareFolder": "Κοινόχρηστο φάκελο",
718717
"Size": "Μέγεθος",
719718
"StartingSSH/SFTPSession": "Έναρξη συνεδρίας SFTP...",
@@ -2543,6 +2542,7 @@
25432542
"GPU": "GPU",
25442543
"GPU(MEM)": "GPU (μνήμη)",
25452544
"Gaudi2Enabled": "Gaudi 2 NPU Ενεργοποιημένο",
2545+
"GoToUploadSessionList": "Μετάβαση στις συνεδρίες μεταφόρτωσης",
25462546
"GracePeriod": "Περίοδος χάριτος",
25472547
"GracePeriodDesc": "Ο ελεγκτής αδράνειας χρήσης θα ενεργοποιηθεί μετά από αυτόν τον αρχικό χρόνο χάριτος. Κατά τη διάρκεια αυτού του χρόνου, οι σύνοδοι δεν τερματίζονται ακόμη και αν η χρησιμοποίηση είναι χαμηλή.",
25482548
"Host": "Πλήθος",

resources/i18n/en.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -719,8 +719,6 @@
719719
"NoSharedFolders": "No shared folders",
720720
"NoSharedUsers": "No one has been invited",
721721
"NotEnoughResourceForFileBrowserSession": "No enough resources(cpu: 1 Core, mem: 0.5GB) to create the session for filebrowser. please check the available resources.",
722-
"NumberOfSFTPSessionsExceededBody": "You are running all available upload sessions you are allowed to create. Please terminated unused upload sessions before starting a new session.",
723-
"NumberOfSFTPSessionsExceededTitle": "Reached limit of running upload session count",
724722
"People": "People",
725723
"Permission": "Permission",
726724
"Permissions": "Permissions",
@@ -731,7 +729,8 @@
731729
"RenameAFolder": "Rename Folder",
732730
"RetryingOperation": "Retrying to establish session...",
733731
"RunSSH/SFTPserver": "Run SFTP server",
734-
"SFTPSessionNotAvailable": "SFTP Session is not available now",
732+
"SFTPSessionCreationFailed": "Failed to create SFTP session",
733+
"SFTPSessionFailureHint": "If many upload sessions are still running, closing unused ones may free up resources on the SFTP host.",
735734
"ShareFolder": "Share Folder",
736735
"Size": "Size",
737736
"StartingSSH/SFTPSession": "Starting SFTP session...",
@@ -2571,6 +2570,7 @@
25712570
"GPU": "GPU",
25722571
"GPU(MEM)": "GPU(Memory)",
25732572
"Gaudi2Enabled": "Gaudi 2 NPU Enabled",
2573+
"GoToUploadSessionList": "Go to upload sessions",
25742574
"GracePeriod": "Grace Period",
25752575
"GracePeriodDesc": "Utilization idle checker will be activated after this initial grace time. During this time, sessions are not terminated even if utilization is low.",
25762576
"Host": "Host",

resources/i18n/es.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -701,8 +701,6 @@
701701
"NoSharedFolders": "Sin carpetas compartidas",
702702
"NoSharedUsers": "No se ha invitado a nadie.",
703703
"NotEnoughResourceForFileBrowserSession": "No hay recursos suficientes (cpu: 1 Core, mem: 0.5GB) para crear la sesión para filebrowser. por favor compruebe los recursos disponibles.",
704-
"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.",
705-
"NumberOfSFTPSessionsExceededTitle": "Se ha alcanzado el límite del recuento de sesiones de carga en ejecución",
706704
"People": "Personas",
707705
"Permission": "Permiso",
708706
"Permissions": "Permisos",
@@ -713,7 +711,8 @@
713711
"RenameAFolder": "Renombrar carpeta",
714712
"RetryingOperation": "Vuelva a intentar establecer sesión ...",
715713
"RunSSH/SFTPserver": "Ejecutar servidor SFTP",
716-
"SFTPSessionNotAvailable": "SFTP Session no está disponible ahora",
714+
"SFTPSessionCreationFailed": "Error al crear la sesión SFTP",
715+
"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.",
717716
"ShareFolder": "Compartir carpeta",
718717
"Size": "Talla",
719718
"StartingSSH/SFTPSession": "Iniciando sesión SFTP...",
@@ -2543,6 +2542,7 @@
25432542
"GPU": "GPU",
25442543
"GPU(MEM)": "GPU (memoria)",
25452544
"Gaudi2Enabled": "Gaudi 2 NPU Activado",
2545+
"GoToUploadSessionList": "Ir a sesiones de carga",
25462546
"GracePeriod": "Período de gracia",
25472547
"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.",
25482548
"Host": "Anfitrión",

resources/i18n/fi.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -701,8 +701,6 @@
701701
"NoSharedFolders": "Ei jaettuja kansioita",
702702
"NoSharedUsers": "Ketään ei ole kutsuttu.",
703703
"NotEnoughResourceForFileBrowserSession": "Resurssit eivät riitä (cpu: 1 Core, mem: 0.5GB) filebrowser-istunnon luomiseen. tarkista käytettävissä olevat resurssit.",
704-
"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.",
705-
"NumberOfSFTPSessionsExceededTitle": "Käynnissä olevan latausistunnon lukumäärän raja saavutettu",
706704
"People": "Ihmiset",
707705
"Permission": "Lupa",
708706
"Permissions": "Luvat",
@@ -713,7 +711,8 @@
713711
"RenameAFolder": "Nimeä kansio uudelleen",
714712
"RetryingOperation": "Uudelleenjärjestelmä istunnon perustamiseksi ...",
715713
"RunSSH/SFTPserver": "Käynnistä SFTP-palvelin",
716-
"SFTPSessionNotAvailable": "SFTP-istunto ei ole nyt käytettävissä",
714+
"SFTPSessionCreationFailed": "SFTP-istunnon luominen epäonnistui",
715+
"SFTPSessionFailureHint": "Jos monia lataamisistuntoja on yhä käynnissä, käyttämättömien sulkeminen voi vapauttaa resursseja SFTP-palvelimella.",
717716
"ShareFolder": "Jaa kansio",
718717
"Size": "Koko",
719718
"StartingSSH/SFTPSession": "SFTP-istunnon käynnistäminen...",
@@ -2543,6 +2542,7 @@
25432542
"GPU": "GPU",
25442543
"GPU(MEM)": "GPU (muisti)",
25452544
"Gaudi2Enabled": "Gaudi 2 NPU Käytössä",
2545+
"GoToUploadSessionList": "Siirry lataamisistuntoihin",
25462546
"GracePeriod": "Karenssiaika",
25472547
"GracePeriodDesc": "Käyttötarkkailu aktivoituu tämän alkuvapauden jälkeen. Tänä aikana istuntoja ei lopeteta, vaikka käyttöaste olisi alhainen.",
25482548
"Host": "Isäntä",

resources/i18n/fr.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -701,8 +701,6 @@
701701
"NoSharedFolders": "Aucun dossier partagé",
702702
"NoSharedUsers": "Personne n'a été invité.",
703703
"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.",
704-
"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.",
705-
"NumberOfSFTPSessionsExceededTitle": "Limite atteinte pour le nombre de sessions de téléchargement en cours",
706704
"People": "Gens",
707705
"Permission": "Autorisation",
708706
"Permissions": "Autorisations",
@@ -713,7 +711,8 @@
713711
"RenameAFolder": "Renommer le dossier",
714712
"RetryingOperation": "Réessayant pour établir la session ...",
715713
"RunSSH/SFTPserver": "Exécuter le serveur SFTP",
716-
"SFTPSessionNotAvailable": "SFTP Session n'est pas disponible pour le moment",
714+
"SFTPSessionCreationFailed": "Échec de la création de la session SFTP",
715+
"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.",
717716
"ShareFolder": "Dossier de partage",
718717
"Size": "Taille",
719718
"StartingSSH/SFTPSession": "Démarrage de la session SFTP...",
@@ -2545,6 +2544,7 @@
25452544
"GPU": "GPU",
25462545
"GPU(MEM)": "GPU (mémoire)",
25472546
"Gaudi2Enabled": "Gaudi 2 NPU Activé",
2547+
"GoToUploadSessionList": "Aller aux sessions d'envoi",
25482548
"GracePeriod": "Délai de grâce",
25492549
"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.",
25502550
"Host": "Hôte",

0 commit comments

Comments
 (0)