Skip to content

Commit ac620df

Browse files
committed
Merge branch 'main' into 26.7
2 parents 8b424ff + bbe959c commit ac620df

29 files changed

Lines changed: 370 additions & 108 deletions
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import BAIFlex from './BAIFlex';
2+
import BAIListAlert from './BAIListAlert';
3+
import type { Meta, StoryObj } from '@storybook/react-vite';
4+
import * as _ from 'lodash-es';
5+
6+
const meta: Meta<typeof BAIListAlert> = {
7+
title: 'Alert/BAIListAlert',
8+
component: BAIListAlert,
9+
tags: ['autodocs'],
10+
parameters: {
11+
layout: 'padded',
12+
docs: {
13+
description: {
14+
component: `
15+
**BAIListAlert** extends **BAIAlert** (and therefore [Ant Design Alert](https://ant.design/components/alert)).
16+
17+
It renders a standardized \`ul\` list inside the alert description — used to
18+
summarize a list of items (e.g. selected resources) inside a modal. The list
19+
scrolls vertically once it exceeds \`maxHeight\`, so the modal never grows
20+
unbounded. Item count indication belongs in the consumer-provided \`title\`
21+
prop (i18n \`count\` interpolation).
22+
23+
## BAI-Specific Props
24+
| Prop | Type | Default | Description |
25+
|------|------|---------|-------------|
26+
| \`items\` | \`Array<{ key?: React.Key; content: ReactNode }>\` | — | List entries rendered as \`li\` elements. \`key\` falls back to the array index |
27+
| \`maxHeight\` | \`CSSProperties['maxHeight']\` | \`165\` | Maximum height of the list before it scrolls vertically |
28+
29+
For all other props, refer to **BAIAlert** and [Ant Design Alert](https://ant.design/components/alert).
30+
`,
31+
},
32+
},
33+
},
34+
argTypes: {
35+
items: {
36+
control: false,
37+
description:
38+
'List entries rendered as li elements; key falls back to the array index',
39+
table: {
40+
type: { summary: 'Array<{ key?: React.Key; content: ReactNode }>' },
41+
},
42+
},
43+
maxHeight: {
44+
control: { type: 'number' },
45+
description: 'Maximum height of the list before it scrolls vertically',
46+
table: {
47+
type: { summary: "CSSProperties['maxHeight']" },
48+
defaultValue: { summary: '165' },
49+
},
50+
},
51+
// Hide Ant Design props used in args
52+
type: { table: { disable: true } },
53+
title: { table: { disable: true } },
54+
showIcon: { table: { disable: true } },
55+
},
56+
};
57+
58+
export default meta;
59+
type Story = StoryObj<typeof BAIListAlert>;
60+
61+
// Default story: Use args for interactive Controls
62+
export const Default: Story = {
63+
name: 'Basic',
64+
args: {
65+
type: 'info',
66+
title: 'The following projects will be updated',
67+
showIcon: true,
68+
items: [
69+
{ key: 'a', content: 'project-alpha' },
70+
{ key: 'b', content: 'project-beta' },
71+
{ key: 'c', content: 'project-gamma' },
72+
],
73+
},
74+
parameters: {
75+
docs: {
76+
description: {
77+
story: 'Basic list summary inside an info alert.',
78+
},
79+
},
80+
},
81+
};
82+
83+
export const LongListWithScroll: Story = {
84+
args: {
85+
type: 'warning',
86+
title: 'The following 30 users will be updated',
87+
showIcon: true,
88+
items: _.map(_.range(30), (i) => ({
89+
key: i,
90+
content: `user-${i + 1}@example.com`,
91+
})),
92+
},
93+
parameters: {
94+
docs: {
95+
description: {
96+
story:
97+
'A long list is capped at the default maxHeight (165px) and scrolls vertically, keeping the surrounding modal compact.',
98+
},
99+
},
100+
},
101+
};
102+
103+
export const CustomMaxHeight: Story = {
104+
args: {
105+
type: 'warning',
106+
title: 'Custom maxHeight of 80px',
107+
showIcon: true,
108+
maxHeight: 80,
109+
items: _.map(_.range(10), (i) => ({
110+
key: i,
111+
content: `item-${i + 1}`,
112+
})),
113+
},
114+
parameters: {
115+
docs: {
116+
description: {
117+
story: 'The scroll cap can be adjusted via the maxHeight prop.',
118+
},
119+
},
120+
},
121+
};
122+
123+
export const AlertTypes: Story = {
124+
render: () => (
125+
<BAIFlex direction="column" gap="md" align="stretch">
126+
<BAIListAlert
127+
type="warning"
128+
showIcon
129+
title="Warning: these users will be updated"
130+
items={[
131+
{ key: 1, content: 'admin@example.com' },
132+
{ key: 2, content: 'user@example.com' },
133+
]}
134+
/>
135+
<BAIListAlert
136+
type="info"
137+
showIcon
138+
ghostInfoBg={false}
139+
title="Info: these projects will be updated"
140+
items={[
141+
{ key: 1, content: 'project-alpha' },
142+
{ key: 2, content: 'project-beta' },
143+
]}
144+
/>
145+
</BAIFlex>
146+
),
147+
parameters: {
148+
docs: {
149+
description: {
150+
story:
151+
'Side-by-side comparison of warning and info (ghostInfoBg disabled) variants, matching the modal call sites.',
152+
},
153+
},
154+
},
155+
};
156+
157+
export const TitleWithCount: Story = {
158+
args: {
159+
type: 'info',
160+
showIcon: true,
161+
ghostInfoBg: false,
162+
title: '3 folders are excluded because they cannot be deleted',
163+
items: [
164+
{ key: 'f1', content: 'shared-folder' },
165+
{ key: 'f2', content: 'model-store' },
166+
{ key: 'f3', content: 'pipeline-data' },
167+
],
168+
},
169+
parameters: {
170+
docs: {
171+
description: {
172+
story:
173+
'Item count indication stays in the consumer-provided title (i18n count interpolation) — the component does not render counts itself.',
174+
},
175+
},
176+
},
177+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import BAIListAlert from './BAIListAlert';
2+
import '@testing-library/jest-dom';
3+
import { render, screen } from '@testing-library/react';
4+
5+
describe('BAIListAlert', () => {
6+
it('should render title and list items', () => {
7+
render(
8+
<BAIListAlert
9+
type="warning"
10+
title="Following users will be updated"
11+
items={[
12+
{ key: '1', content: 'a@example.com' },
13+
{ key: '2', content: 'b@example.com' },
14+
]}
15+
/>,
16+
);
17+
expect(
18+
screen.getByText('Following users will be updated'),
19+
).toBeInTheDocument();
20+
expect(screen.getByText('a@example.com')).toBeInTheDocument();
21+
expect(screen.getByText('b@example.com')).toBeInTheDocument();
22+
expect(screen.getAllByRole('listitem')).toHaveLength(2);
23+
});
24+
25+
it('should apply the default maxHeight with vertical scroll', () => {
26+
render(<BAIListAlert items={[{ content: 'item' }]} />);
27+
const list = screen.getByRole('list');
28+
expect(list).toHaveStyle({ maxHeight: '165px', overflowY: 'auto' });
29+
});
30+
31+
it('should apply a custom maxHeight', () => {
32+
render(<BAIListAlert maxHeight={80} items={[{ content: 'item' }]} />);
33+
expect(screen.getByRole('list')).toHaveStyle({ maxHeight: '80px' });
34+
});
35+
36+
it('should render items without explicit keys (index fallback)', () => {
37+
render(
38+
<BAIListAlert items={[{ content: 'first' }, { content: 'second' }]} />,
39+
);
40+
expect(screen.getByText('first')).toBeInTheDocument();
41+
expect(screen.getByText('second')).toBeInTheDocument();
42+
});
43+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import BAIAlert, { BAIAlertProps } from './BAIAlert';
2+
import { theme } from 'antd';
3+
import { createStyles } from 'antd-style';
4+
import * as _ from 'lodash-es';
5+
import React, { ReactNode } from 'react';
6+
7+
export interface BAIListAlertItem {
8+
key?: React.Key | null;
9+
content: ReactNode;
10+
}
11+
12+
export interface BAIListAlertProps extends Omit<BAIAlertProps, 'description'> {
13+
items: Array<BAIListAlertItem>;
14+
maxHeight?: React.CSSProperties['maxHeight'];
15+
}
16+
17+
const useStyles = createStyles(({ css, token }) => ({
18+
// scrollbar with no track background — only the thumb floats over content
19+
transparentScrollbar: css`
20+
/* Firefox, Chrome 121+ — thumb / track */
21+
scrollbar-color: ${token.colorTextQuaternary} transparent;
22+
scrollbar-width: thin;
23+
24+
/* Safari and older WebKit (ignored where scrollbar-color is supported) */
25+
&::-webkit-scrollbar,
26+
&::-webkit-scrollbar-track {
27+
background: transparent;
28+
}
29+
&::-webkit-scrollbar {
30+
width: 6px;
31+
}
32+
&::-webkit-scrollbar-thumb {
33+
background: ${token.colorTextQuaternary};
34+
border-radius: 3px;
35+
}
36+
`,
37+
}));
38+
39+
/**
40+
* Alert that summarizes a list of items (e.g. selected resources in a modal)
41+
* as a standardized `ul` inside the alert description. The list scrolls
42+
* vertically once it exceeds `maxHeight`, so the surrounding modal never
43+
* grows unbounded. Item count indication belongs in the consumer-provided
44+
* `title` prop (i18n `count` interpolation).
45+
*/
46+
const BAIListAlert: React.FC<BAIListAlertProps> = ({
47+
items,
48+
// ~7 rows of list content; inherited from the pre-extraction
49+
// UpdateUsersModal style that this component standardizes.
50+
maxHeight = 165,
51+
...alertProps
52+
}) => {
53+
'use memo';
54+
const { token } = theme.useToken();
55+
const { styles } = useStyles();
56+
return (
57+
<BAIAlert
58+
{...alertProps}
59+
description={
60+
_.isEmpty(items) ? undefined : (
61+
<ul
62+
// make the scrollable region reachable by keyboard
63+
tabIndex={0}
64+
className={styles.transparentScrollbar}
65+
style={{
66+
margin: 0,
67+
padding: 0,
68+
paddingTop: token.paddingXXS,
69+
listStyle: 'circle',
70+
listStylePosition: 'inside',
71+
maxHeight,
72+
overflowY: 'auto',
73+
}}
74+
>
75+
{_.map(items, (item, index) => (
76+
<li key={item.key ?? `__index-${index}`}>{item.content}</li>
77+
))}
78+
</ul>
79+
)
80+
}
81+
/>
82+
);
83+
};
84+
85+
export default BAIListAlert;

packages/backend.ai-ui/src/components/fragments/BAIProjectBulkEditModal.tsx

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { BAIProjectBulkEditModalFragment$key } from '../../__generated__/BAIProj
22
import { BAIProjectBulkEditModalProjectMutation } from '../../__generated__/BAIProjectBulkEditModalProjectMutation.graphql';
33
import { useMutationWithPromise } from '../../hooks';
44
import { useBAIi18n } from '../../hooks/useBAIi18n';
5-
import BAIAlert from '../BAIAlert';
65
import BAIFlex from '../BAIFlex';
6+
import BAIListAlert from '../BAIListAlert';
77
import BAIModal, { BAIModalProps } from '../BAIModal';
88
import BAISelect from '../BAISelect';
99
import BAIProjectResourcePolicySelect from './BAIProjectResourcePolicySelect';
10-
import { Form, theme } from 'antd';
10+
import { Form } from 'antd';
1111
import * as _ from 'lodash-es';
1212
import { Suspense, useState } from 'react';
1313
import { graphql, useFragment } from 'react-relay';
@@ -21,7 +21,6 @@ const BAIProjectBulkEditModal = ({
2121
...tableProps
2222
}: BAIProjectBulkEditModalProps) => {
2323
const { t } = useBAIi18n();
24-
const { token } = theme.useToken();
2524
const [form] = Form.useForm();
2625
const [isSaving, setIsSaving] = useState(false);
2726
const mutateProjectWithPromise =
@@ -77,27 +76,17 @@ const BAIProjectBulkEditModal = ({
7776
destroyOnHidden
7877
>
7978
<BAIFlex direction="column" align="stretch" gap="md">
80-
<BAIAlert
79+
<BAIListAlert
8180
type="info"
8281
showIcon
8382
ghostInfoBg={false}
8483
title={t(
8584
'comp:BAIProjectBulkEditModal.FollowingProjectsWillBeUpdated',
8685
)}
87-
description={
88-
<ul
89-
style={{
90-
margin: 0,
91-
padding: 0,
92-
paddingTop: token.paddingXXS,
93-
listStyle: 'circle',
94-
}}
95-
>
96-
{_.map(selectedProjects, (project) => (
97-
<li key={project.row_id}>{project.name}</li>
98-
))}
99-
</ul>
100-
}
86+
items={_.map(selectedProjects, (project) => ({
87+
key: project.row_id,
88+
content: project.name,
89+
}))}
10190
/>
10291
<Form form={form}>
10392
<Suspense

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export { default as BAIDynamicUnitInputNumberWithSlider } from './BAIDynamicUnit
8080
export type { BAIDynamicUnitInputNumberWithSliderProps } from './BAIDynamicUnitInputNumberWithSlider';
8181
export { default as BAIAlert } from './BAIAlert';
8282
export type { BAIAlertProps } from './BAIAlert';
83+
export { default as BAIListAlert } from './BAIListAlert';
84+
export type { BAIListAlertProps, BAIListAlertItem } from './BAIListAlert';
8385
export { default as BAIProjectResourceGroupSelect } from './BAIProjectResourceGroupSelect';
8486
export { default as BAITextHighlighter } from './BAITextHighlighter';
8587
export { default as BooleanTag } from './BooleanTag';

0 commit comments

Comments
 (0)