Files
gpustack-ui/src/pages/_components/select-panel/index.tsx
T
2026-04-16 15:03:59 +08:00

225 lines
6.1 KiB
TypeScript

import { SearchOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Empty, Input } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import List from './list';
import SelectedList from './selected-list';
const PanelWrapper = styled.div<{ $maxHeight?: number; $leftWidth?: number }>`
border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius);
overflow-y: auto;
max-height: ${({ $maxHeight }) =>
$maxHeight ? `${$maxHeight + 2}px` : 'auto'};
`;
const Left = styled.div`
padding: 0;
`;
const Right = styled.div``;
const Header = styled.div`
padding: 8px 12px 8px;
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--ant-color-split);
background-color: var(--ant-color-fill-alter);
`;
interface SelectPanelProps {
searchPlaceholder?: string;
height?: number;
leftWidth?: number;
options: Array<{ key: string; title: string }>;
selectedKeys: string[];
notFoundContent?: React.ReactNode;
onSelectChange: (selectedKeys: string[]) => void;
styles?: {
container?: React.CSSProperties;
left?: React.CSSProperties;
right?: React.CSSProperties;
header?: React.CSSProperties;
};
}
const SelectPanel: React.FC<SelectPanelProps> = ({
height = 300,
leftWidth = 260,
options,
selectedKeys,
searchPlaceholder,
notFoundContent,
styles,
onSelectChange
}) => {
const intl = useIntl();
const [indeterminate, setIndeterminate] = React.useState(false);
const [checkAll, setCheckAll] = React.useState(false);
const [searchText, setSearchText] = useState('');
const showOptions = useMemo(() => {
return options.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
);
}, [options, searchText]);
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(e.target.value);
};
const handleOnUnselect = (
key: string,
newSelectedKeys: { key: string; title: string }[]
) => {
onSelectChange(newSelectedKeys.map((item) => item.key));
};
const handleCheckAllChange = (e: any) => {
const checked = e.target.checked;
setCheckAll(checked);
setIndeterminate(false);
if (checked) {
const allKeys = options
.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
)
.map((item) => item.key);
onSelectChange(Array.from(new Set([...selectedKeys, ...allKeys])));
} else {
const filteredKeys = options
.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
)
.map((item) => item.key);
const newSelectedKeys = selectedKeys.filter(
(key) => !filteredKeys.includes(key)
);
onSelectChange(newSelectedKeys);
}
};
const updateCheckStatus = (newSelectedKeys: string[]) => {
if (options.length === 0) {
setIndeterminate(false);
setCheckAll(false);
return;
}
const filteredOptions = options.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
);
const filteredKeys = filteredOptions.map((item) => item.key);
const selectedFilteredKeys = newSelectedKeys.filter((key) =>
filteredKeys.includes(key)
);
setIndeterminate(
selectedFilteredKeys.length > 0 &&
selectedFilteredKeys.length < filteredKeys.length
);
setCheckAll(selectedFilteredKeys.length === filteredKeys.length);
};
const handleSelectChange = (newSelectedKeys: string[]) => {
onSelectChange(newSelectedKeys);
updateCheckStatus(newSelectedKeys);
};
const handleClearSelection = () => {
onSelectChange([]);
setCheckAll(false);
setIndeterminate(false);
};
useEffect(() => {
updateCheckStatus(selectedKeys);
}, [selectedKeys, options]);
const renderRight = () => {
return (
<Right>
<Header>
<span>({selectedKeys.length}) selected</span>
<Button type="text" size="small" onClick={handleClearSelection}>
Clear
</Button>
</Header>
<SelectedList
maxHeight={height - 50}
selectedList={options.filter((item) =>
selectedKeys.includes(item.key)
)}
onUnselect={handleOnUnselect}
/>
</Right>
);
};
return (
<PanelWrapper
$maxHeight={height}
$leftWidth={leftWidth}
style={styles?.container}
>
<Left>
<Header style={styles?.header}>
<span
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
color: 'var(--ant-color-text-tertiary)'
}}
>
<Checkbox
checked={checkAll}
indeterminate={indeterminate}
onChange={handleCheckAllChange}
></Checkbox>
<span>
{intl.formatMessage(
{ id: 'common.select.count' },
{ count: selectedKeys.length }
)}
</span>
</span>
<Input
prefix={
<SearchOutlined
style={{ color: 'var(--ant-color-text-quaternary)' }}
/>
}
size="small"
allowClear
status={'null' as any}
placeholder={searchPlaceholder}
style={{
width: 300,
height: 32,
borderRadius: 4,
backgroundColor: 'var(--ant-color-bg-container) !important'
}}
onChange={handleSearch}
/>
</Header>
{showOptions.length > 0 ? (
<List
maxHeight={height - 50}
dataList={showOptions}
selectedKeys={selectedKeys}
onSelectChange={handleSelectChange}
/>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={notFoundContent}
></Empty>
)}
</Left>
</PanelWrapper>
);
};
export default SelectPanel;