Skip to content

Commit 718ce52

Browse files
committed
fix(FR-1261): Show fallback UI if there is storage proxy crash issue on faulty node (#3978)
Resolves #3977 ([FR-1261](https://lablup.atlassian.net/browse/FR-1261)) # Improve error handling in storage and resource group components This PR adds error boundaries and graceful error handling to storage and resource group related components to prevent UI crashes when backend API calls fail. Key changes: - Replaced `useSuspenseTanQuery` with `useTanQuery` in `ResourceGroupSelect` and `StorageSelect` components - Added proper error handling with fallbacks in API calls - Added type definitions for better code maintainability - Modified `useCurrentProject` hook to handle vhost info fetch failures gracefully - Added proper fallback for alerts in `MainLayout` - Added error boundaries to storage-related cards in `VFolderNodeListPage` These changes ensure that storage-related UI components continue to function even when backend API calls fail, providing a more resilient user experience. **Checklist:** - [ ] Documentation - [ ] Minium required manager version - [x] Specific setting for review (eg., KB link, endpoint or how to setup): http://10.122.12.219:8090 - [x] Minimum requirements to check during review: see whole pages without error boundary. - [ ] Test case(s) to demonstrate the difference of before/after [FR-1261]: https://lablup.atlassian.net/browse/FR-1261?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 6fce92d commit 718ce52

29 files changed

+248
-117
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { theme, Tooltip } from 'antd';
2+
import { TooltipPropsWithTitle } from 'antd/es/tooltip';
3+
import { CircleAlertIcon } from 'lucide-react';
4+
import React from 'react';
5+
6+
interface BAIAlertIconWithTooltipProps
7+
extends Omit<TooltipPropsWithTitle, 'children'> {
8+
iconProps?: React.ComponentProps<typeof CircleAlertIcon>;
9+
type?: 'warning' | 'error';
10+
}
11+
const BAIAlertIconWithTooltip = ({
12+
iconProps,
13+
type = 'error',
14+
...tooltipProps
15+
}: BAIAlertIconWithTooltipProps) => {
16+
const { token } = theme.useToken();
17+
return (
18+
<Tooltip {...tooltipProps}>
19+
<CircleAlertIcon
20+
style={{
21+
color: type === 'warning' ? token.colorWarning : token.colorError,
22+
cursor: 'help',
23+
}}
24+
{...iconProps}
25+
/>
26+
</Tooltip>
27+
);
28+
};
29+
30+
export default BAIAlertIconWithTooltip;

packages/backend.ai-ui/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export { default as BAIResourceWithSteppedProgress } from './BAIResourceWithStep
1717
export { default as BAIRowWrapWithDividers } from './BAIRowWrapWithDividers';
1818
export type { BAIResourceWithSteppedProgressProps } from './BAIResourceWithSteppedProgress';
1919
export { default as BAIUnmountAfterClose } from './BAIUnmountAfterClose';
20+
export { default as BAIAlertIconWithTooltip } from './BAIAlertIconWithTooltip';
2021
export * from './Table';
2122
export * from './fragments';
2223
export * from './provider';

react/src/components/MainLayout/MainLayout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { BAIFlex } from 'backend.ai-ui';
1919
import { atom, useSetAtom } from 'jotai';
2020
import _ from 'lodash';
2121
import { Suspense, useEffect, useLayoutEffect, useRef, useState } from 'react';
22+
import { ErrorBoundary } from 'react-error-boundary';
2223
import { useNavigate, Outlet, useMatches } from 'react-router-dom';
2324

2425
export const HEADER_Z_INDEX_IN_MAIN_LAYOUT = 5;
@@ -219,14 +220,16 @@ function MainLayout() {
219220
</div>
220221
</Suspense>
221222
{/* Non sticky Alert components */}
222-
<Suspense>
223+
<Suspense fallback={<div style={{ minHeight: '0px' }} />}>
223224
<BAIFlex
224225
direction="column"
225226
gap={'sm'}
226227
align="stretch"
227228
className={styles.alertWrapper}
228229
>
229-
<NoResourceGroupAlert />
230+
<ErrorBoundary fallbackRender={() => null}>
231+
<NoResourceGroupAlert />
232+
</ErrorBoundary>
230233
<PasswordChangeRequestAlert
231234
showIcon
232235
icon={undefined}

react/src/components/ResourceGroupSelect.tsx

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ import { SelectProps } from 'antd';
88
import _ from 'lodash';
99
import React, { useEffect, useState, useTransition } from 'react';
1010

11+
interface ScalingGroupItem {
12+
name: string;
13+
}
14+
15+
interface VolumeInfo {
16+
backend: string;
17+
capabilities: string[];
18+
usage: {
19+
percentage: number;
20+
};
21+
sftp_scaling_groups?: string[];
22+
}
23+
1124
interface ResourceGroupSelectProps extends BAISelectProps {
1225
projectName: string;
1326
autoSelectDefault?: boolean;
@@ -47,27 +60,19 @@ const ResourceGroupSelect: React.FC<ResourceGroupSelectProps> = ({
4760
);
4861

4962
const { data: resourceGroupSelectQueryResult } = useSuspenseTanQuery<
50-
[
51-
{
52-
scaling_groups: {
53-
name: string;
54-
}[];
55-
},
56-
{
57-
allowed: string[];
58-
default: string;
59-
volume_info: {
60-
[key: string]: {
61-
backend: string;
62-
capabilities: string[];
63-
usage: {
64-
percentage: number;
65-
};
66-
sftp_scaling_groups?: string[];
63+
| [
64+
{
65+
scaling_groups: ScalingGroupItem[];
66+
},
67+
{
68+
allowed: string[];
69+
default: string;
70+
volume_info: {
71+
[key: string]: VolumeInfo;
6772
};
68-
};
69-
},
70-
]
73+
},
74+
]
75+
| null
7176
>({
7277
queryKey: ['ResourceGroupSelectQuery', projectName],
7378
queryFn: () => {
@@ -84,18 +89,18 @@ const ResourceGroupSelect: React.FC<ResourceGroupSelectProps> = ({
8489
}),
8590
]);
8691
},
87-
staleTime: 0,
92+
staleTime: 1000 * 60 * 5, // Cache for 5 minutes
8893
fetchKey: fetchKey,
8994
});
9095

9196
const sftpResourceGroups = _.flatMap(
92-
resourceGroupSelectQueryResult?.[1].volume_info,
97+
resourceGroupSelectQueryResult?.[1]?.volume_info,
9398
(item) => item?.sftp_scaling_groups ?? [],
9499
);
95100

96101
const resourceGroups = _.filter(
97-
resourceGroupSelectQueryResult?.[0].scaling_groups,
98-
(item) => {
102+
resourceGroupSelectQueryResult?.[0]?.scaling_groups,
103+
(item: ScalingGroupItem) => {
99104
if (_.includes(sftpResourceGroups, item.name)) {
100105
return false;
101106
}

react/src/components/StorageSelect.tsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ export type VolumeInfo = {
1919
};
2020
sftp_scaling_groups: string[];
2121
};
22+
23+
interface VHostInfo {
24+
default: string;
25+
allowed: Array<string>;
26+
volume_info?: {
27+
[key: string]: VolumeInfo;
28+
};
29+
}
30+
2231
interface Props extends Omit<BAISelectProps, 'value' | 'onChange'> {
2332
autoSelectType?: 'usage' | 'default';
2433
showUsageStatus?: boolean;
@@ -41,20 +50,7 @@ const StorageSelect: React.FC<Props> = ({
4150
const baiClient = useSuspendedBackendaiClient();
4251

4352
const { data: vhostInfo, isLoading: isLoadingVhostInfo } =
44-
useSuspenseTanQuery<{
45-
default: string;
46-
allowed: Array<string>;
47-
volume_info?: {
48-
[key: string]: {
49-
backend: string;
50-
capabilities: string[];
51-
usage: {
52-
percentage: number;
53-
};
54-
sftp_scaling_groups: any[];
55-
};
56-
};
57-
}>({
53+
useSuspenseTanQuery<VHostInfo | null>({
5854
queryKey: ['vhostInfo'],
5955
queryFn: () => {
6056
return baiClient.vfolder.list_hosts();
@@ -69,7 +65,7 @@ const StorageSelect: React.FC<Props> = ({
6965
const [controllableSearchValue, setControllableSearchValue] =
7066
useControllableState({ value: searchValue, onChange: onSearch });
7167
useEffect(() => {
72-
if (!autoSelectType) return;
68+
if (!autoSelectType || !vhostInfo) return; // Return early if vhostInfo is null
7369
let nextHost = vhostInfo?.default ?? vhostInfo?.allowed[0] ?? '';
7470
if (autoSelectType === 'usage') {
7571
const lowestUsageHost = _.minBy(

react/src/hooks/useCurrentProject.tsx

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ import { atomWithDefault } from 'jotai/utils';
44
import _ from 'lodash';
55
import { useCallback, useEffect } from 'react';
66

7+
interface ScalingGroupItem {
8+
name: string;
9+
}
10+
11+
interface VHostVolumeInfo {
12+
backend: string;
13+
capabilities: string[];
14+
usage: {
15+
percentage: number;
16+
};
17+
sftp_scaling_groups?: string[];
18+
}
19+
20+
interface VHostInfo {
21+
allowed: string[];
22+
default: string;
23+
volume_info: {
24+
[key: string]: VHostVolumeInfo;
25+
};
26+
}
27+
28+
interface ScalingGroupsResponse {
29+
scaling_groups: ScalingGroupItem[];
30+
}
31+
732
const currentProjectAtom = atomWithDefault(() => {
833
return {
934
// @ts-ignore
@@ -22,7 +47,7 @@ const previousSelectedResourceGroupNameAtom = atom<string | null>(null);
2247

2348
export const useCurrentResourceGroupValue = () => {
2449
useSuspendedBackendaiClient();
25-
const { resourceGroups } = useAtomValue(resourceGroupsForCurrentProjectAtom);
50+
const { resourceGroups } = useResourceGroupsForCurrentProject();
2651
const [prevSelectedRGName, setPrevSelectedRGName] = useAtom(
2752
previousSelectedResourceGroupNameAtom,
2853
);
@@ -68,31 +93,18 @@ const resourceGroupsForCurrentProjectAtom = atom(async (get) => {
6893
const currentProject = get(currentProjectAtom);
6994
const [resourceGroups, vhostInfo] = await Promise.all([
7095
// @ts-ignore
71-
globalThis.backendaiclient.scalingGroup.list(currentProject.name) as {
72-
scaling_groups: {
73-
name: string;
74-
}[];
75-
},
96+
globalThis.backendaiclient.scalingGroup.list(
97+
currentProject.name,
98+
) as ScalingGroupsResponse,
7699
// @ts-ignore
77-
globalThis.backendaiclient.vfolder.list_hosts(currentProject.id) as {
78-
allowed: string[];
79-
default: string;
80-
volume_info: {
81-
[key: string]: {
82-
backend: string;
83-
capabilities: string[];
84-
usage: {
85-
percentage: number;
86-
};
87-
sftp_scaling_groups?: string[];
88-
};
89-
};
90-
},
100+
globalThis.backendaiclient.vfolder.list_hosts(
101+
currentProject.id,
102+
) as VHostInfo,
91103
]);
92104

93105
const allSftpScalingGroups = _.uniq(
94106
_.flatten(
95-
_.map(vhostInfo.volume_info, (volume) => volume.sftp_scaling_groups),
107+
_.map(vhostInfo.volume_info, (volume) => volume?.sftp_scaling_groups),
96108
),
97109
);
98110

react/src/pages/ComputeSessionListPage.tsx

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
BAISessionsIcon,
4545
BAIPropertyFilter,
4646
mergeFilterValues,
47+
BAIAlertIconWithTooltip,
4748
} from 'backend.ai-ui';
4849
import _ from 'lodash';
4950
import { PowerOffIcon } from 'lucide-react';
@@ -55,6 +56,7 @@ import {
5556
useRef,
5657
useState,
5758
} from 'react';
59+
import { ErrorBoundary } from 'react-error-boundary';
5860
import { useTranslation } from 'react-i18next';
5961
import { graphql, useLazyLoadQuery } from 'react-relay';
6062
import { useLocation } from 'react-router-dom';
@@ -341,30 +343,49 @@ const ComputeSessionListPage = () => {
341343
</BAICard>
342344
</Col>
343345
<Col xs={24} lg={16} xl={20} style={{ display: 'flex' }}>
344-
<Suspense
345-
fallback={
346-
<BAICard
346+
<ErrorBoundary
347+
fallbackRender={() => {
348+
return (
349+
<BAICard
350+
style={{
351+
width: '100%',
352+
minHeight: lg ? CARD_MIN_HEIGHT : undefined,
353+
}}
354+
status="error"
355+
extra={
356+
<BAIAlertIconWithTooltip
357+
title={t('error.UnexpectedError')}
358+
/>
359+
}
360+
/>
361+
);
362+
}}
363+
>
364+
<Suspense
365+
fallback={
366+
<BAICard
367+
style={{
368+
width: '100%',
369+
minHeight: lg ? CARD_MIN_HEIGHT : undefined,
370+
}}
371+
loading
372+
/>
373+
}
374+
>
375+
<ConfigurableResourceCard
347376
style={{
348377
width: '100%',
349378
minHeight: lg ? CARD_MIN_HEIGHT : undefined,
350379
}}
351-
loading
380+
isRefetching={deferredFetchKey !== fetchKey}
381+
fetchKey={deferredFetchKey}
382+
queryRef={
383+
queryRef.TotalResourceWithinResourceGroupFragment ?? undefined
384+
}
385+
onResourceGroupChange={setCurrentResourceGroup}
352386
/>
353-
}
354-
>
355-
<ConfigurableResourceCard
356-
style={{
357-
width: '100%',
358-
minHeight: lg ? CARD_MIN_HEIGHT : undefined,
359-
}}
360-
isRefetching={deferredFetchKey !== fetchKey}
361-
fetchKey={deferredFetchKey}
362-
queryRef={
363-
queryRef.TotalResourceWithinResourceGroupFragment ?? undefined
364-
}
365-
onResourceGroupChange={setCurrentResourceGroup}
366-
/>
367-
</Suspense>
387+
</Suspense>
388+
</ErrorBoundary>
368389
</Col>
369390
</Row>
370391
<BAICard

0 commit comments

Comments
 (0)