Skip to content

Commit a78c902

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 082b3a1 commit a78c902

3 files changed

Lines changed: 131 additions & 21 deletions

File tree

react/src/components/STokenLoginBoundary.tsx

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { createBackendAIClient, tokenLogin } from '../helper/loginSessionAuth';
1515
import { useResolvedApiEndpoint } from '../hooks/useResolvedApiEndpoint';
1616
import { loginConfigState } from '../hooks/useWebUIConfig';
1717
import { jotaiStore } from './DefaultProviders';
18-
import { Alert, Button, Card } from 'antd';
19-
import { BAIFlex, useBAILogger } from 'backend.ai-ui';
18+
import { App, Spin, Typography } from 'antd';
19+
import { BAIButton, BAICard, BAIFlex, useBAILogger } from 'backend.ai-ui';
2020
import { useAtomValue } from 'jotai';
2121
import {
2222
Suspense,
@@ -226,8 +226,21 @@ const STokenLoginBoundaryInner: React.FC<STokenLoginBoundaryProps> = ({
226226
};
227227

228228
/**
229-
* Placeholder connecting card. FR-2632 replaces this with the polished
230-
* BAICard-based version + i18n keys.
229+
* Map an error kind to its PascalCase i18n key suffix. The i18n schema
230+
* restricts message keys to a flat two-level shape (`module.Key`) where
231+
* `Key` must begin with an uppercase letter, so kinds like
232+
* `missing-token` cannot appear verbatim in the key.
233+
*/
234+
const kindToI18nKey = (kind: STokenLoginError['kind']): string =>
235+
kind
236+
.split('-')
237+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
238+
.join('');
239+
240+
/**
241+
* Connecting card shown while the authentication sequence is in flight.
242+
* The card is visually subdued so reviewers can tell the app is still
243+
* working but hasn't failed.
231244
*/
232245
const DefaultFallback: React.FC = () => {
233246
const { t } = useTranslation();
@@ -236,41 +249,100 @@ const DefaultFallback: React.FC = () => {
236249
direction="column"
237250
align="center"
238251
justify="center"
239-
style={{ minHeight: '60vh' }}
252+
style={{ minHeight: '60vh', padding: 24 }}
240253
>
241-
<Card>{t('login.ConnectingToCluster')}</Card>
254+
<BAICard
255+
style={{ maxWidth: 480, width: '100%', textAlign: 'center' }}
256+
bordered
257+
>
258+
<BAIFlex direction="column" align="center" gap="md">
259+
<Spin size="large" />
260+
<Typography.Title level={5} style={{ margin: 0 }}>
261+
{t('sTokenLoginBoundary.AuthenticatingTitle')}
262+
</Typography.Title>
263+
<Typography.Text type="secondary">
264+
{t('sTokenLoginBoundary.AuthenticatingDescription')}
265+
</Typography.Text>
266+
</BAIFlex>
267+
</BAICard>
242268
</BAIFlex>
243269
);
244270
};
245271

246272
/**
247-
* Placeholder error card. FR-2632 replaces this with the full BAICard +
248-
* BAIButton UI (Retry with async loading state, Copy error details).
273+
* Built-in error card rendered when `errorFallback` is not provided.
274+
* Offers two actions: Retry (runs the sequence again via BAIButton's
275+
* async `action` prop so the loading state appears automatically) and
276+
* Copy details (serializes the `{ kind, cause }` payload to JSON and
277+
* writes it to the clipboard for support follow-up).
249278
*/
250279
const DefaultErrorCard: React.FC<{
251280
error: STokenLoginError;
252281
onRetry: () => void;
253282
}> = ({ error, onRetry }) => {
283+
const { t } = useTranslation();
284+
const { message } = App.useApp();
285+
286+
const kindKey = kindToI18nKey(error.kind);
287+
const title = t(`sTokenLoginBoundary.Error${kindKey}Title`);
288+
const description = t(`sTokenLoginBoundary.Error${kindKey}Description`);
289+
const causeDetail =
290+
'cause' in error && error.cause
291+
? String((error.cause as Error)?.message ?? error.cause)
292+
: null;
293+
294+
const handleRetry = useCallback(async () => {
295+
// Wrap in a Promise so BAIButton.action triggers its async loading
296+
// state; the synchronous state reset completes before the next
297+
// render, which is visually indistinguishable from the live
298+
// sequence restart.
299+
await Promise.resolve();
300+
onRetry();
301+
}, [onRetry]);
302+
303+
const handleCopy = useCallback(async () => {
304+
const payload = {
305+
kind: error.kind,
306+
cause: causeDetail,
307+
};
308+
await navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
309+
message.success(t('sTokenLoginBoundary.ErrorDetailsCopied'));
310+
}, [error, causeDetail, message, t]);
311+
254312
return (
255313
<BAIFlex
256314
direction="column"
257315
align="center"
258316
justify="center"
259317
style={{ minHeight: '60vh', padding: 24 }}
260318
>
261-
<Alert
262-
type="error"
263-
title={`sToken login failed: ${error.kind}`}
264-
description={
265-
'cause' in error && error.cause
266-
? String((error.cause as Error)?.message ?? error.cause)
267-
: undefined
268-
}
269-
style={{ maxWidth: 520, marginBottom: 16 }}
270-
/>
271-
<Button type="primary" onClick={onRetry}>
272-
Retry
273-
</Button>
319+
<BAICard
320+
status="error"
321+
title={title}
322+
style={{ maxWidth: 520, width: '100%' }}
323+
>
324+
<BAIFlex direction="column" gap="md" align="stretch">
325+
<Typography.Paragraph style={{ margin: 0 }}>
326+
{description}
327+
</Typography.Paragraph>
328+
{causeDetail && (
329+
<Typography.Paragraph
330+
type="secondary"
331+
style={{ margin: 0, fontSize: 12, whiteSpace: 'pre-wrap' }}
332+
>
333+
{causeDetail}
334+
</Typography.Paragraph>
335+
)}
336+
<BAIFlex direction="row" gap="sm" justify="end">
337+
<BAIButton onClick={handleCopy}>
338+
{t('sTokenLoginBoundary.CopyErrorDetails')}
339+
</BAIButton>
340+
<BAIButton type="primary" action={handleRetry}>
341+
{t('sTokenLoginBoundary.Retry')}
342+
</BAIButton>
343+
</BAIFlex>
344+
</BAIFlex>
345+
</BAICard>
274346
</BAIFlex>
275347
);
276348
};

resources/i18n/en.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,6 +2076,25 @@
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+
"ErrorEndpointUnresolvedDescription": "The Backend.AI server address could not be resolved from the configuration file. Please try again in a moment or contact your administrator.",
2087+
"ErrorEndpointUnresolvedTitle": "Server configuration is unavailable.",
2088+
"ErrorMissingTokenDescription": "The sign-in token is missing from the request. Try opening the link again from your original source, or contact your administrator.",
2089+
"ErrorMissingTokenTitle": "No sign-in token was provided.",
2090+
"ErrorServerUnreachableDescription": "The server did not respond to the initial connection. Check your network and try again.",
2091+
"ErrorServerUnreachableTitle": "Cannot reach the Backend.AI server.",
2092+
"ErrorTokenInvalidDescription": "The sign-in token may have expired or been revoked. Please restart the sign-in flow from your original application.",
2093+
"ErrorTokenInvalidTitle": "Your sign-in token is not valid.",
2094+
"ErrorUnknownDescription": "An unexpected error occurred while signing in. Please try again, and copy the error details below if the problem persists.",
2095+
"ErrorUnknownTitle": "Sign-in failed unexpectedly.",
2096+
"Retry": "Retry"
2097+
},
20792098
"scanArtifactModelsFromHuggingFaceModal": {
20802099
"EnterAModelID": "Enter a model ID. (e.g. openai/gpt-oss-20b)",
20812100
"EnterAVersion": "Enter a version. (default: main)",

resources/i18n/ko.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2078,6 +2078,25 @@
20782078
"SharedMemory": "공유 메모리",
20792079
"Updated": "자원 프리셋이 수정되었습니다"
20802080
},
2081+
"sTokenLoginBoundary": {
2082+
"AuthenticatingDescription": "싱글 사인온 토큰으로 로그인하고 있습니다. 잠시만 기다려 주세요.",
2083+
"AuthenticatingTitle": "로그인 중",
2084+
"CopyErrorDetails": "오류 세부 정보 복사",
2085+
"ErrorConcurrentSessionDescription": "다른 곳에서 이미 로그인되어 있습니다. 해당 세션을 종료한 뒤 다시 시도해 주세요.",
2086+
"ErrorConcurrentSessionTitle": "이미 다른 세션에서 로그인된 상태입니다.",
2087+
"ErrorDetailsCopied": "오류 세부 정보가 클립보드에 복사되었습니다.",
2088+
"ErrorEndpointUnresolvedDescription": "구성 파일에서 Backend.AI 서버 주소를 확인하지 못했습니다. 잠시 후 다시 시도하거나 관리자에게 문의하세요.",
2089+
"ErrorEndpointUnresolvedTitle": "서버 설정을 불러올 수 없습니다.",
2090+
"ErrorMissingTokenDescription": "요청에 로그인 토큰이 포함되어 있지 않습니다. 원래 링크를 다시 열거나 관리자에게 문의하세요.",
2091+
"ErrorMissingTokenTitle": "로그인 토큰이 없습니다.",
2092+
"ErrorServerUnreachableDescription": "초기 연결에 서버가 응답하지 않았습니다. 네트워크 상태를 확인한 뒤 다시 시도하세요.",
2093+
"ErrorServerUnreachableTitle": "Backend.AI 서버에 연결할 수 없습니다.",
2094+
"ErrorTokenInvalidDescription": "로그인 토큰이 만료되었거나 해지되었을 수 있습니다. 원래 애플리케이션에서 로그인 절차를 다시 시작해 주세요.",
2095+
"ErrorTokenInvalidTitle": "로그인 토큰이 유효하지 않습니다.",
2096+
"ErrorUnknownDescription": "로그인 중 알 수 없는 오류가 발생했습니다. 다시 시도해 주시고, 문제가 계속되면 아래 오류 세부 정보를 복사해 전달해 주세요.",
2097+
"ErrorUnknownTitle": "로그인 중 예기치 않은 오류가 발생했습니다.",
2098+
"Retry": "다시 시도"
2099+
},
20812100
"scanArtifactModelsFromHuggingFaceModal": {
20822101
"EnterAModelID": "모델 ID를 입력하십시오. (예 : openai/gpt-oss-20b)",
20832102
"EnterAVersion": "버전을 입력하십시오. (기본값 : main)",

0 commit comments

Comments
 (0)