Skip to content

Commit 9ae4ae8

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 ddc7039 commit 9ae4ae8

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
@@ -19,8 +19,8 @@ import {
1919
import { useResolvedApiEndpoint } from '../hooks/useResolvedApiEndpoint';
2020
import { loginConfigState } from '../hooks/useWebUIConfig';
2121
import { jotaiStore } from './DefaultProviders';
22-
import { Alert, Button, Card } from 'antd';
23-
import { BAIFlex, useBAILogger } from 'backend.ai-ui';
22+
import { App, Spin, Typography } from 'antd';
23+
import { BAIButton, BAICard, BAIFlex, useBAILogger } from 'backend.ai-ui';
2424
import { useAtomValue } from 'jotai';
2525
import {
2626
Suspense,
@@ -257,8 +257,21 @@ const STokenLoginBoundaryInner: React.FC<STokenLoginBoundaryProps> = ({
257257
};
258258

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

277303
/**
278-
* Placeholder error card. FR-2632 replaces this with the full BAICard +
279-
* BAIButton UI (Retry with async loading state, Copy error details).
304+
* Built-in error card rendered when `errorFallback` is not provided.
305+
* Offers two actions: Retry (runs the sequence again via BAIButton's
306+
* async `action` prop so the loading state appears automatically) and
307+
* Copy details (serializes the `{ kind, cause }` payload to JSON and
308+
* writes it to the clipboard for support follow-up).
280309
*/
281310
const DefaultErrorCard: React.FC<{
282311
error: STokenLoginError;
283312
onRetry: () => void;
284313
}> = ({ error, onRetry }) => {
314+
const { t } = useTranslation();
315+
const { message } = App.useApp();
316+
317+
const kindKey = kindToI18nKey(error.kind);
318+
const title = t(`sTokenLoginBoundary.Error${kindKey}Title`);
319+
const description = t(`sTokenLoginBoundary.Error${kindKey}Description`);
320+
const causeDetail =
321+
'cause' in error && error.cause
322+
? String((error.cause as Error)?.message ?? error.cause)
323+
: null;
324+
325+
const handleRetry = useCallback(async () => {
326+
// Wrap in a Promise so BAIButton.action triggers its async loading
327+
// state; the synchronous state reset completes before the next
328+
// render, which is visually indistinguishable from the live
329+
// sequence restart.
330+
await Promise.resolve();
331+
onRetry();
332+
}, [onRetry]);
333+
334+
const handleCopy = useCallback(async () => {
335+
const payload = {
336+
kind: error.kind,
337+
cause: causeDetail,
338+
};
339+
await navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
340+
message.success(t('sTokenLoginBoundary.ErrorDetailsCopied'));
341+
}, [error, causeDetail, message, t]);
342+
285343
return (
286344
<BAIFlex
287345
direction="column"
288346
align="center"
289347
justify="center"
290348
style={{ minHeight: '60vh', padding: 24 }}
291349
>
292-
<Alert
293-
type="error"
294-
title={`sToken login failed: ${error.kind}`}
295-
description={
296-
'cause' in error && error.cause
297-
? String((error.cause as Error)?.message ?? error.cause)
298-
: undefined
299-
}
300-
style={{ maxWidth: 520, marginBottom: 16 }}
301-
/>
302-
<Button type="primary" onClick={onRetry}>
303-
Retry
304-
</Button>
350+
<BAICard
351+
status="error"
352+
title={title}
353+
style={{ maxWidth: 520, width: '100%' }}
354+
>
355+
<BAIFlex direction="column" gap="md" align="stretch">
356+
<Typography.Paragraph style={{ margin: 0 }}>
357+
{description}
358+
</Typography.Paragraph>
359+
{causeDetail && (
360+
<Typography.Paragraph
361+
type="secondary"
362+
style={{ margin: 0, fontSize: 12, whiteSpace: 'pre-wrap' }}
363+
>
364+
{causeDetail}
365+
</Typography.Paragraph>
366+
)}
367+
<BAIFlex direction="row" gap="sm" justify="end">
368+
<BAIButton onClick={handleCopy}>
369+
{t('sTokenLoginBoundary.CopyErrorDetails')}
370+
</BAIButton>
371+
<BAIButton type="primary" action={handleRetry}>
372+
{t('sTokenLoginBoundary.Retry')}
373+
</BAIButton>
374+
</BAIFlex>
375+
</BAIFlex>
376+
</BAICard>
305377
</BAIFlex>
306378
);
307379
};

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)