Skip to content

Commit 1129d3d

Browse files
committed
feat(FR-2632): add default fallback and error card UI for STokenLoginBoundary
Replace the placeholder BAICard stub and the ad-hoc Alert-based error view with the full UI described in spec section '에러 처리 & 기본 에러 카드 UI': - DefaultFallback: centered BAICard with Spin + localized title and description. Shown while the authentication sequence is running. - DefaultErrorCard: BAICard with status='error' header, translated per-kind title and description, and optional cause detail. Actions are 'Copy error details' (serializes { kind, cause } to JSON, writes to clipboard, toasts a confirmation) and 'Retry' using BAIButton's async action prop so the loading state renders automatically while the sequence restarts. i18n keys added under sTokenLoginBoundary.* in en.json and ko.json; the project's i18n schema only allows two-level nesting so kinds are mapped to PascalCase suffixes (ErrorMissingTokenTitle etc.) via a small helper in the component. Other language files will be filled by /fw:i18n in a follow-up. Refs FR-2616
1 parent 546add2 commit 1129d3d

22 files changed

Lines changed: 520 additions & 21 deletions

react/src/components/STokenLoginBoundary.tsx

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import {
2020
import { useResolvedApiEndpoint } from '../hooks/useResolvedApiEndpoint';
2121
import { loginConfigState } from '../hooks/useWebUIConfig';
2222
import { jotaiStore } from './DefaultProviders';
23-
import { Alert, Button, Card } from 'antd';
24-
import { BAIFlex, useBAILogger } from 'backend.ai-ui';
23+
import { App, Spin, Typography } from 'antd';
24+
import { BAIButton, BAICard, BAIFlex, useBAILogger } from 'backend.ai-ui';
2525
import { useAtomValue } from 'jotai';
2626
import {
2727
Suspense,
@@ -261,8 +261,21 @@ const STokenLoginBoundaryInner: React.FC<STokenLoginBoundaryProps> = ({
261261
};
262262

263263
/**
264-
* Placeholder connecting card. FR-2632 replaces this with the polished
265-
* BAICard-based version + i18n keys.
264+
* Map an error kind to its PascalCase i18n key suffix. The i18n schema
265+
* restricts message keys to a flat two-level shape (`module.Key`) where
266+
* `Key` must begin with an uppercase letter, so kinds like
267+
* `missing-token` cannot appear verbatim in the key.
268+
*/
269+
const kindToI18nKey = (kind: STokenLoginError['kind']): string =>
270+
kind
271+
.split('-')
272+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
273+
.join('');
274+
275+
/**
276+
* Connecting card shown while the authentication sequence is in flight.
277+
* The card is visually subdued so reviewers can tell the app is still
278+
* working but hasn't failed.
266279
*/
267280
const DefaultFallback: React.FC = () => {
268281
const { t } = useTranslation();
@@ -271,41 +284,107 @@ const DefaultFallback: React.FC = () => {
271284
direction="column"
272285
align="center"
273286
justify="center"
274-
style={{ minHeight: '60vh' }}
287+
style={{ minHeight: '60vh', padding: 24 }}
275288
>
276-
<Card>{t('login.ConnectingToCluster')}</Card>
289+
<BAICard
290+
style={{ maxWidth: 480, width: '100%', textAlign: 'center' }}
291+
bordered
292+
>
293+
<BAIFlex direction="column" align="center" gap="md">
294+
<Spin size="large" />
295+
<Typography.Title level={5} style={{ margin: 0 }}>
296+
{t('sTokenLoginBoundary.AuthenticatingTitle')}
297+
</Typography.Title>
298+
<Typography.Text type="secondary">
299+
{t('sTokenLoginBoundary.AuthenticatingDescription')}
300+
</Typography.Text>
301+
</BAIFlex>
302+
</BAICard>
277303
</BAIFlex>
278304
);
279305
};
280306

281307
/**
282-
* Placeholder error card. FR-2632 replaces this with the full BAICard +
283-
* BAIButton UI (Retry with async loading state, Copy error details).
308+
* Built-in error card rendered when `errorFallback` is not provided.
309+
* Offers two actions: Retry (runs the sequence again via BAIButton's
310+
* async `action` prop so the loading state appears automatically) and
311+
* Copy details (serializes the `{ kind, cause }` payload to JSON and
312+
* writes it to the clipboard for support follow-up).
284313
*/
285314
const DefaultErrorCard: React.FC<{
286315
error: STokenLoginError;
287316
onRetry: () => void;
288317
}> = ({ error, onRetry }) => {
318+
'use memo';
319+
const { t } = useTranslation();
320+
const { message } = App.useApp();
321+
const { logger } = useBAILogger();
322+
323+
const kindKey = kindToI18nKey(error.kind);
324+
const title = t(`sTokenLoginBoundary.Error${kindKey}Title`);
325+
const description = t(`sTokenLoginBoundary.Error${kindKey}Description`);
326+
const causeDetail =
327+
'cause' in error && error.cause
328+
? String((error.cause as Error)?.message ?? error.cause)
329+
: null;
330+
331+
const handleRetry = useCallback(async () => {
332+
// Wrap in a Promise so BAIButton.action triggers its async loading
333+
// state; the synchronous state reset completes before the next
334+
// render, which is visually indistinguishable from the live
335+
// sequence restart.
336+
await Promise.resolve();
337+
onRetry();
338+
}, [onRetry]);
339+
340+
const handleCopy = useCallback(async () => {
341+
const payload = {
342+
kind: error.kind,
343+
cause: causeDetail,
344+
};
345+
try {
346+
await navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
347+
message.success(t('sTokenLoginBoundary.ErrorDetailsCopied'));
348+
} catch (copyError) {
349+
logger.warn('[STokenLoginBoundary] clipboard write failed', copyError);
350+
message.error(t('sTokenLoginBoundary.ErrorDetailsCopyFailed'));
351+
}
352+
}, [error, causeDetail, message, t, logger]);
353+
289354
return (
290355
<BAIFlex
291356
direction="column"
292357
align="center"
293358
justify="center"
294359
style={{ minHeight: '60vh', padding: 24 }}
295360
>
296-
<Alert
297-
type="error"
298-
title={`sToken login failed: ${error.kind}`}
299-
description={
300-
'cause' in error && error.cause
301-
? String((error.cause as Error)?.message ?? error.cause)
302-
: undefined
303-
}
304-
style={{ maxWidth: 520, marginBottom: 16 }}
305-
/>
306-
<Button type="primary" onClick={onRetry}>
307-
Retry
308-
</Button>
361+
<BAICard
362+
status="error"
363+
title={title}
364+
style={{ maxWidth: 520, width: '100%' }}
365+
>
366+
<BAIFlex direction="column" gap="md" align="stretch">
367+
<Typography.Paragraph style={{ margin: 0 }}>
368+
{description}
369+
</Typography.Paragraph>
370+
{causeDetail && (
371+
<Typography.Paragraph
372+
type="secondary"
373+
style={{ margin: 0, fontSize: 12, whiteSpace: 'pre-wrap' }}
374+
>
375+
{causeDetail}
376+
</Typography.Paragraph>
377+
)}
378+
<BAIFlex direction="row" gap="sm" justify="end">
379+
<BAIButton action={handleCopy}>
380+
{t('sTokenLoginBoundary.CopyErrorDetails')}
381+
</BAIButton>
382+
<BAIButton type="primary" action={handleRetry}>
383+
{t('sTokenLoginBoundary.Retry')}
384+
</BAIButton>
385+
</BAIFlex>
386+
</BAIFlex>
387+
</BAICard>
309388
</BAIFlex>
310389
);
311390
};

resources/i18n/de.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,6 +2068,26 @@
20682068
"SharedMemory": "Gemeinsamer Speicher",
20692069
"Updated": "Ressourcenvoreinstellung aktualisiert"
20702070
},
2071+
"sTokenLoginBoundary": {
2072+
"AuthenticatingDescription": "Authenticating with your single sign-on token. This usually takes only a moment.",
2073+
"AuthenticatingTitle": "Signing you in",
2074+
"CopyErrorDetails": "Copy error details",
2075+
"ErrorConcurrentSessionDescription": "This account is already signed in somewhere else. Please close the other session and try again.",
2076+
"ErrorConcurrentSessionTitle": "Another active session was detected.",
2077+
"ErrorDetailsCopied": "Error details copied to clipboard.",
2078+
"ErrorDetailsCopyFailed": "Failed to copy error details. Please copy them manually from the page.",
2079+
"ErrorEndpointUnresolvedDescription": "The Backend.AI server address could not be resolved from the configuration file. Please try again in a moment or contact your administrator.",
2080+
"ErrorEndpointUnresolvedTitle": "Server configuration is unavailable.",
2081+
"ErrorMissingTokenDescription": "The sign-in token is missing from the request. Try opening the link again from your original source, or contact your administrator.",
2082+
"ErrorMissingTokenTitle": "No sign-in token was provided.",
2083+
"ErrorServerUnreachableDescription": "The server did not respond to the initial connection. Check your network and try again.",
2084+
"ErrorServerUnreachableTitle": "Cannot reach the Backend.AI server.",
2085+
"ErrorTokenInvalidDescription": "The sign-in token may have expired or been revoked. Please restart the sign-in flow from your original application.",
2086+
"ErrorTokenInvalidTitle": "Your sign-in token is not valid.",
2087+
"ErrorUnknownDescription": "An unexpected error occurred while signing in. Please try again, and copy the error details below if the problem persists.",
2088+
"ErrorUnknownTitle": "Sign-in failed unexpectedly.",
2089+
"Retry": "Retry"
2090+
},
20712091
"scanArtifactModelsFromHuggingFaceModal": {
20722092
"EnterAModelID": "Geben Sie die Modell-ID ein. (z. B.: openai/gpt-oss-20b)",
20732093
"EnterAVersion": "Geben Sie die Version ein. (Standard: main)",

resources/i18n/el.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,6 +2066,26 @@
20662066
"SharedMemory": "Κοινόχρηστη μνήμη",
20672067
"Updated": "Η προεπιλογή πόρων ενημερώθηκε"
20682068
},
2069+
"sTokenLoginBoundary": {
2070+
"AuthenticatingDescription": "Authenticating with your single sign-on token. This usually takes only a moment.",
2071+
"AuthenticatingTitle": "Signing you in",
2072+
"CopyErrorDetails": "Copy error details",
2073+
"ErrorConcurrentSessionDescription": "This account is already signed in somewhere else. Please close the other session and try again.",
2074+
"ErrorConcurrentSessionTitle": "Another active session was detected.",
2075+
"ErrorDetailsCopied": "Error details copied to clipboard.",
2076+
"ErrorDetailsCopyFailed": "Failed to copy error details. Please copy them manually from the page.",
2077+
"ErrorEndpointUnresolvedDescription": "The Backend.AI server address could not be resolved from the configuration file. Please try again in a moment or contact your administrator.",
2078+
"ErrorEndpointUnresolvedTitle": "Server configuration is unavailable.",
2079+
"ErrorMissingTokenDescription": "The sign-in token is missing from the request. Try opening the link again from your original source, or contact your administrator.",
2080+
"ErrorMissingTokenTitle": "No sign-in token was provided.",
2081+
"ErrorServerUnreachableDescription": "The server did not respond to the initial connection. Check your network and try again.",
2082+
"ErrorServerUnreachableTitle": "Cannot reach the Backend.AI server.",
2083+
"ErrorTokenInvalidDescription": "The sign-in token may have expired or been revoked. Please restart the sign-in flow from your original application.",
2084+
"ErrorTokenInvalidTitle": "Your sign-in token is not valid.",
2085+
"ErrorUnknownDescription": "An unexpected error occurred while signing in. Please try again, and copy the error details below if the problem persists.",
2086+
"ErrorUnknownTitle": "Sign-in failed unexpectedly.",
2087+
"Retry": "Retry"
2088+
},
20692089
"scanArtifactModelsFromHuggingFaceModal": {
20702090
"EnterAModelID": "Εισαγάγετε το ID του μοντέλου. (π.χ.: openai/gpt-oss-20b)",
20712091
"EnterAVersion": "Εισαγάγετε την έκδοση. (Προεπιλογή: main)",

resources/i18n/en.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,6 +2076,26 @@
20762076
"SharedMemory": "Shared Memory",
20772077
"Updated": "Resource preset updated"
20782078
},
2079+
"sTokenLoginBoundary": {
2080+
"AuthenticatingDescription": "Authenticating with your single sign-on token. This usually takes only a moment.",
2081+
"AuthenticatingTitle": "Signing you in",
2082+
"CopyErrorDetails": "Copy error details",
2083+
"ErrorConcurrentSessionDescription": "This account is already signed in somewhere else. Please close the other session and try again.",
2084+
"ErrorConcurrentSessionTitle": "Another active session was detected.",
2085+
"ErrorDetailsCopied": "Error details copied to clipboard.",
2086+
"ErrorDetailsCopyFailed": "Failed to copy error details. Please copy them manually from the page.",
2087+
"ErrorEndpointUnresolvedDescription": "The Backend.AI server address could not be resolved from the configuration file. Please try again in a moment or contact your administrator.",
2088+
"ErrorEndpointUnresolvedTitle": "Server configuration is unavailable.",
2089+
"ErrorMissingTokenDescription": "The sign-in token is missing from the request. Try opening the link again from your original source, or contact your administrator.",
2090+
"ErrorMissingTokenTitle": "No sign-in token was provided.",
2091+
"ErrorServerUnreachableDescription": "The server did not respond to the initial connection. Check your network and try again.",
2092+
"ErrorServerUnreachableTitle": "Cannot reach the Backend.AI server.",
2093+
"ErrorTokenInvalidDescription": "The sign-in token may have expired or been revoked. Please restart the sign-in flow from your original application.",
2094+
"ErrorTokenInvalidTitle": "Your sign-in token is not valid.",
2095+
"ErrorUnknownDescription": "An unexpected error occurred while signing in. Please try again, and copy the error details below if the problem persists.",
2096+
"ErrorUnknownTitle": "Sign-in failed unexpectedly.",
2097+
"Retry": "Retry"
2098+
},
20792099
"scanArtifactModelsFromHuggingFaceModal": {
20802100
"EnterAModelID": "Enter a model ID. (e.g. openai/gpt-oss-20b)",
20812101
"EnterAVersion": "Enter a version. (default: main)",

resources/i18n/es.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,6 +2066,26 @@
20662066
"SharedMemory": "Memoria compartida",
20672067
"Updated": "Preajuste de recursos actualizado"
20682068
},
2069+
"sTokenLoginBoundary": {
2070+
"AuthenticatingDescription": "Authenticating with your single sign-on token. This usually takes only a moment.",
2071+
"AuthenticatingTitle": "Signing you in",
2072+
"CopyErrorDetails": "Copy error details",
2073+
"ErrorConcurrentSessionDescription": "This account is already signed in somewhere else. Please close the other session and try again.",
2074+
"ErrorConcurrentSessionTitle": "Another active session was detected.",
2075+
"ErrorDetailsCopied": "Error details copied to clipboard.",
2076+
"ErrorDetailsCopyFailed": "Failed to copy error details. Please copy them manually from the page.",
2077+
"ErrorEndpointUnresolvedDescription": "The Backend.AI server address could not be resolved from the configuration file. Please try again in a moment or contact your administrator.",
2078+
"ErrorEndpointUnresolvedTitle": "Server configuration is unavailable.",
2079+
"ErrorMissingTokenDescription": "The sign-in token is missing from the request. Try opening the link again from your original source, or contact your administrator.",
2080+
"ErrorMissingTokenTitle": "No sign-in token was provided.",
2081+
"ErrorServerUnreachableDescription": "The server did not respond to the initial connection. Check your network and try again.",
2082+
"ErrorServerUnreachableTitle": "Cannot reach the Backend.AI server.",
2083+
"ErrorTokenInvalidDescription": "The sign-in token may have expired or been revoked. Please restart the sign-in flow from your original application.",
2084+
"ErrorTokenInvalidTitle": "Your sign-in token is not valid.",
2085+
"ErrorUnknownDescription": "An unexpected error occurred while signing in. Please try again, and copy the error details below if the problem persists.",
2086+
"ErrorUnknownTitle": "Sign-in failed unexpectedly.",
2087+
"Retry": "Retry"
2088+
},
20692089
"scanArtifactModelsFromHuggingFaceModal": {
20702090
"EnterAModelID": "Introduzca el ID del modelo. (p. ej.: openai/gpt-oss-20b)",
20712091
"EnterAVersion": "Introduzca la versión. (Predeterminado: main)",

resources/i18n/fi.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,6 +2066,26 @@
20662066
"SharedMemory": "Jaettu muisti",
20672067
"Updated": "Resurssien esiasetus päivitetty"
20682068
},
2069+
"sTokenLoginBoundary": {
2070+
"AuthenticatingDescription": "Authenticating with your single sign-on token. This usually takes only a moment.",
2071+
"AuthenticatingTitle": "Signing you in",
2072+
"CopyErrorDetails": "Copy error details",
2073+
"ErrorConcurrentSessionDescription": "This account is already signed in somewhere else. Please close the other session and try again.",
2074+
"ErrorConcurrentSessionTitle": "Another active session was detected.",
2075+
"ErrorDetailsCopied": "Error details copied to clipboard.",
2076+
"ErrorDetailsCopyFailed": "Failed to copy error details. Please copy them manually from the page.",
2077+
"ErrorEndpointUnresolvedDescription": "The Backend.AI server address could not be resolved from the configuration file. Please try again in a moment or contact your administrator.",
2078+
"ErrorEndpointUnresolvedTitle": "Server configuration is unavailable.",
2079+
"ErrorMissingTokenDescription": "The sign-in token is missing from the request. Try opening the link again from your original source, or contact your administrator.",
2080+
"ErrorMissingTokenTitle": "No sign-in token was provided.",
2081+
"ErrorServerUnreachableDescription": "The server did not respond to the initial connection. Check your network and try again.",
2082+
"ErrorServerUnreachableTitle": "Cannot reach the Backend.AI server.",
2083+
"ErrorTokenInvalidDescription": "The sign-in token may have expired or been revoked. Please restart the sign-in flow from your original application.",
2084+
"ErrorTokenInvalidTitle": "Your sign-in token is not valid.",
2085+
"ErrorUnknownDescription": "An unexpected error occurred while signing in. Please try again, and copy the error details below if the problem persists.",
2086+
"ErrorUnknownTitle": "Sign-in failed unexpectedly.",
2087+
"Retry": "Retry"
2088+
},
20692089
"scanArtifactModelsFromHuggingFaceModal": {
20702090
"EnterAModelID": "Syötä mallin tunnus (esim. openai/gpt-oss-20b)",
20712091
"EnterAVersion": "Syötä versio. (Oletus: main)",

resources/i18n/fr.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,6 +2068,26 @@
20682068
"SharedMemory": "Mémoire partagée",
20692069
"Updated": "Préréglage de ressource mis à jour"
20702070
},
2071+
"sTokenLoginBoundary": {
2072+
"AuthenticatingDescription": "Authenticating with your single sign-on token. This usually takes only a moment.",
2073+
"AuthenticatingTitle": "Signing you in",
2074+
"CopyErrorDetails": "Copy error details",
2075+
"ErrorConcurrentSessionDescription": "This account is already signed in somewhere else. Please close the other session and try again.",
2076+
"ErrorConcurrentSessionTitle": "Another active session was detected.",
2077+
"ErrorDetailsCopied": "Error details copied to clipboard.",
2078+
"ErrorDetailsCopyFailed": "Failed to copy error details. Please copy them manually from the page.",
2079+
"ErrorEndpointUnresolvedDescription": "The Backend.AI server address could not be resolved from the configuration file. Please try again in a moment or contact your administrator.",
2080+
"ErrorEndpointUnresolvedTitle": "Server configuration is unavailable.",
2081+
"ErrorMissingTokenDescription": "The sign-in token is missing from the request. Try opening the link again from your original source, or contact your administrator.",
2082+
"ErrorMissingTokenTitle": "No sign-in token was provided.",
2083+
"ErrorServerUnreachableDescription": "The server did not respond to the initial connection. Check your network and try again.",
2084+
"ErrorServerUnreachableTitle": "Cannot reach the Backend.AI server.",
2085+
"ErrorTokenInvalidDescription": "The sign-in token may have expired or been revoked. Please restart the sign-in flow from your original application.",
2086+
"ErrorTokenInvalidTitle": "Your sign-in token is not valid.",
2087+
"ErrorUnknownDescription": "An unexpected error occurred while signing in. Please try again, and copy the error details below if the problem persists.",
2088+
"ErrorUnknownTitle": "Sign-in failed unexpectedly.",
2089+
"Retry": "Retry"
2090+
},
20712091
"scanArtifactModelsFromHuggingFaceModal": {
20722092
"EnterAModelID": "Saisissez l'ID du modèle. (ex : openai/gpt-oss-20b)",
20732093
"EnterAVersion": "Saisissez la version. (Par défaut : main)",

0 commit comments

Comments
 (0)