feat: add simple select component

This commit is contained in:
jialin
2025-07-01 20:20:50 +08:00
parent 7da106798d
commit 9e29e7283c
12 changed files with 339 additions and 129 deletions
+147
View File
@@ -0,0 +1,147 @@
import { useIntl } from '@umijs/max';
import type { SelectProps } from 'antd';
import { Checkbox, Select, Tag } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React from 'react';
import styled from 'styled-components';
import AutoTooltip from '../auto-tooltip';
const OptionWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;
const DropdownWrapper = styled.div`
.ant-select-item {
padding-inline-start: 8px;
&:hover {
background-color: var(--ant-select-option-active-bg);
}
}
.ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background-color: unset;
font-weight: unset;
&:hover {
background-color: var(--ant-select-option-active-bg);
}
}
`;
const SelectAllWrapper = styled.div`
margin-bottom: 10px;
padding: 5px 8px;
font-size: 12px;
border-bottom: 1px solid var(--ant-color-split);
.ant-checkbox-wrapper {
color: var(--ant-color-text-tertiary);
}
`;
const TagWrapper = styled(Tag)`
border-radius: 12px;
`;
const SimpleSelect: React.FC<SelectProps> = (props) => {
const intl = useIntl();
const { options, ...restProps } = props;
const [allSelection, setAllSelection] = React.useState<{
checked: boolean;
indeterminate: boolean;
}>({
checked: false,
indeterminate: false
});
const optionRender = (option: any, info: any) => {
const { value, label } = option;
return (
<OptionWrapper>
{restProps.value?.includes(value) ? (
<Checkbox checked></Checkbox>
) : (
<Checkbox></Checkbox>
)}
<AutoTooltip ghost>{label}</AutoTooltip>
</OptionWrapper>
);
};
const handleOnCheckboxChange = (e: CheckboxChangeEvent) => {
const isChecked = e.target.checked;
const allValues = options?.map((opt: any) => opt.value) || [];
setAllSelection({
checked: isChecked,
indeterminate: false
});
restProps.onChange?.(isChecked ? allValues : [], options || []);
};
const dropdownRender = (originPanel: React.ReactNode) => {
return (
<DropdownWrapper>
{restProps.mode === 'multiple' && (
<SelectAllWrapper>
<Checkbox
checked={allSelection.checked}
indeterminate={allSelection.indeterminate}
onChange={handleOnCheckboxChange}
>
{intl.formatMessage({ id: 'common.checbox.all' })}
</Checkbox>
</SelectAllWrapper>
)}
{originPanel}
</DropdownWrapper>
);
};
const handleOnChange = (value: any, option: any) => {
const selectedValues = Array.isArray(value) ? value : [value];
const allSelected = options?.map((opt: any) => opt.value) || [];
const isAllSelected = selectedValues.length === allSelected?.length;
setAllSelection({
checked: isAllSelected,
indeterminate: !isAllSelected && selectedValues.length > 0
});
restProps.onChange?.(selectedValues, option);
};
const TagRender = (props: any) => {
const { label } = props;
const count = props.isMaxTag ? label.slice(0, -3).slice(1) : label;
return (
<TagWrapper
bordered={false}
style={{
height: 24,
backgroundColor: 'var(--ant-color-fill-tertiary)',
fontSize: 'var(--ant-font-size)'
}}
className="flex-center"
>
{intl.formatMessage({ id: 'common.select.count' }, { count: count })}
</TagWrapper>
);
};
return (
<Select
{...restProps}
options={options}
maxTagCount={0}
dropdownRender={dropdownRender}
optionRender={optionRender}
menuItemSelectedIcon={false}
onChange={handleOnChange}
tagRender={TagRender}
></Select>
);
};
export default SimpleSelect;
+3 -2
View File
@@ -97,7 +97,7 @@ export default {
'common.form.field.input.required': 'required',
'common.form.field.select.required': 'required',
'common.select.option': 'All',
'common.checbox.all': 'All',
'common.checbox.all': 'Select all',
'common.select.all': 'All {type}',
'common.data.unkonwn': 'Unknown',
'common.data.none': 'No Data',
@@ -250,5 +250,6 @@ export default {
'Oops! Something went wrong. Try refreshing the page.',
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection'
'common.button.clearSelection': 'Clear Selection',
'common.select.count': '{count} selected'
};
+4 -2
View File
@@ -250,7 +250,8 @@ export default {
'Oops! Something went wrong. Try refreshing the page.',
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection'
'common.button.clearSelection': 'Clear Selection',
'common.select.count': '{count} selected'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -267,5 +268,6 @@ export default {
// 11. 'common.page.wentwrong': 'Something went wrong.',
// 12. 'common.page.refresh.tips': 'Oops! Something went wrong. Try refreshing the page.'
// 13. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.'
// 14. 'common.button.clearSelection': 'Clear Selection'
// 14. 'common.button.clearSelection': 'Clear Selection',
// 15. 'common.select.count': '{count} selected'
// ========== End of To-Do List ==========
+3 -1
View File
@@ -249,9 +249,11 @@ export default {
'Упс! Что-то пошло не так. Попробуйте обновить страницу.',
'common.tips.escape.disable':
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
'common.button.clearSelection': 'Clear Selection'
'common.button.clearSelection': 'Clear Selection',
'common.select.count': '{count} selected'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'common.button.clearSelection': 'Clear Selection'
// 2. 'common.select.count': '{count} selected'
// ========== End of To-Do List ==========
+2 -1
View File
@@ -243,5 +243,6 @@ export default {
'common.page.wentwrong': '哎呀,出了点问题',
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
'common.button.clearSelection': '清除选择'
'common.button.clearSelection': '清除选择',
'common.select.count': '已选 {count} 项'
};
@@ -8,6 +8,7 @@ import dayjs from 'dayjs';
import React, { useEffect } from 'react';
import { DASHBOARD_USAGE_API } from '../../apis';
import { TableRow } from '../../config/types';
import FilterBar from './filter-bar';
import useUsageData from './use-usage-data';
const ExportData: React.FC<{
@@ -17,7 +18,6 @@ const ExportData: React.FC<{
const { open, onCancel } = props || {};
const intl = useIntl();
const {
FilterBar,
init,
setResult,
loading,
@@ -25,7 +25,11 @@ const ExportData: React.FC<{
userList,
modelList,
query,
setQuery
setQuery,
handleExport,
handleDateChange,
handleUsersChange,
handleModelsChange
} = useUsageData<{
items: TableRow[];
}>({
@@ -169,7 +173,16 @@ const ExportData: React.FC<{
></ModalFooter>
}
>
<FilterBar></FilterBar>
<FilterBar
disabledDate={false}
url={DASHBOARD_USAGE_API}
query={query}
userList={userList}
modelList={modelList}
handleDateChange={handleDateChange}
handleUsersChange={handleUsersChange}
handleModelsChange={handleModelsChange}
></FilterBar>
<Table
columns={exportTableColumns}
tableLayout={'auto'}
@@ -0,0 +1,121 @@
import SimpleSelect from '@/components/seal-form/simple-select';
import { DownloadOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, DatePicker, Tooltip } from 'antd';
import dayjs from 'dayjs';
import React from 'react';
import styled from 'styled-components';
import { DASHBOARD_STATS_API } from '../../apis';
import useRangePickerPreset from '../../hooks/use-rangepicker-preset';
const DefaultDateConfig = {
maxRange: 60,
defaultRange: 29
};
const FilterWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0px;
.selection {
display: flex;
align-items: center;
gap: 12px;
}
.ant-select-selection-overflow-item > span {
height: 24px;
}
`;
interface FilterBarProps {
query: any;
userList: any[];
modelList: any[];
handleDateChange: (dates: any, dateStrings: [string, string]) => void;
handleUsersChange: (value: any) => void;
handleModelsChange: (value: any) => void;
handleExport?: () => void;
url: string;
disabledDate?: boolean;
}
const FilterBar: React.FC<FilterBarProps> = (props) => {
const {
query,
userList,
modelList,
handleDateChange,
handleUsersChange,
handleModelsChange,
handleExport,
url,
disabledDate
} = props;
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({
range: DefaultDateConfig.maxRange,
disabledDate: disabledDate
});
const intl = useIntl();
const filterOptions = (inputValue: any, option: any) => {
return option.label?.toLowerCase().includes(inputValue.toLowerCase());
};
return (
<FilterWrapper>
<div className="selection">
<DatePicker.RangePicker
maxDate={dayjs()}
defaultValue={[
dayjs().add(-DefaultDateConfig.defaultRange, 'd'),
dayjs()
]}
disabledDate={disabledRangeDaysDate}
presets={rangePresets}
allowClear={false}
style={{ width: 220 }}
value={[dayjs(query.start_date), dayjs(query.end_date)]}
onChange={handleDateChange}
></DatePicker.RangePicker>
<SimpleSelect
allowClear
showSearch
mode="multiple"
options={userList}
maxTagCount={0}
filterOption={filterOptions}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectuser'
})}
style={{ maxWidth: 200, minWidth: 160 }}
value={query.user_ids}
onChange={handleUsersChange}
></SimpleSelect>
<SimpleSelect
allowClear
showSearch
mode="multiple"
options={modelList}
maxTagCount={0}
filterOption={filterOptions}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectmodel'
})}
value={query.model_ids}
style={{ maxWidth: 200, minWidth: 160 }}
onChange={handleModelsChange}
></SimpleSelect>
{url === DASHBOARD_STATS_API && (
<Tooltip title={intl.formatMessage({ id: 'common.button.export' })}>
<Button icon={<DownloadOutlined />} onClick={handleExport}></Button>
</Tooltip>
)}
</div>
</FilterWrapper>
);
};
export default FilterBar;
@@ -8,6 +8,7 @@ import { baseColorMap } from '../../config';
import { DashboardContext } from '../../config/dashboard-context';
import { DashboardUsageData } from '../../config/types';
import ExportData from './export-data';
import FilterBar from './filter-bar';
import RequestTokenInner from './request-token-inner';
import TopUser from './top-user';
import useUsageData from './use-usage-data';
@@ -21,16 +22,27 @@ const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
const intl = useIntl();
const { model_usage } = useContext(DashboardContext);
const { usageData, handleOnCancel, handleExport, FilterBar, init, open } =
useUsageData<DashboardUsageData>({
url: DASHBOARD_STATS_API,
disabledDate: true,
defaultData: {
api_request_history: [],
completion_token_history: [],
prompt_token_history: []
}
});
const {
usageData,
query,
userList,
modelList,
handleOnCancel,
init,
handleExport,
handleDateChange,
handleUsersChange,
handleModelsChange,
open
} = useUsageData<DashboardUsageData>({
url: DASHBOARD_STATS_API,
disabledDate: true,
defaultData: {
api_request_history: [],
completion_token_history: [],
prompt_token_history: []
}
});
const topUserData = useMemo(() => {
// top 10 users
@@ -104,10 +116,19 @@ const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
<TitleWrapper>
{intl.formatMessage({ id: 'dashboard.usage' })}
</TitleWrapper>
<FilterBar></FilterBar>
<FilterBar
url={DASHBOARD_STATS_API}
query={query}
userList={userList}
modelList={modelList}
disabledDate={true}
handleDateChange={handleDateChange}
handleUsersChange={handleUsersChange}
handleModelsChange={handleModelsChange}
handleExport={handleExport}
></FilterBar>
</div>
<RequestTokenInner
onExport={handleExport}
requestData={usageData?.requestTokenData.requestData}
xAxisData={usageData?.requestTokenData.xAxisData}
tokenData={usageData?.requestTokenData.tokenData}
@@ -27,7 +27,6 @@ const CardWrapperBox = styled.div`
`;
interface RequestTokenInnerProps {
onExport?: () => void;
requestData: {
name: string;
color: string;
@@ -1,36 +1,11 @@
import useSelectRender from '@/components/seal-form/hooks/use-select-render';
import { queryModelsList } from '@/pages/llmodels/apis';
import { ListItem as ModelListItem } from '@/pages/llmodels/config/types';
import { queryUsersList } from '@/pages/users/apis';
import { DownloadOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, DatePicker, Select, Tooltip } from 'antd';
import dayjs from 'dayjs';
import _ from 'lodash';
import { useMemo, useState } from 'react';
import styled from 'styled-components';
import {
DASHBOARD_STATS_API,
DASHBOARD_USAGE_API,
queryDashboardUsageData
} from '../../apis';
import { DASHBOARD_USAGE_API, queryDashboardUsageData } from '../../apis';
import { baseColorMap } from '../../config';
import useRangePickerPreset from '../../hooks/use-rangepicker-preset';
const FilterWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0px;
.selection {
display: flex;
align-items: center;
gap: 12px;
}
.ant-select-selection-overflow-item > span {
height: 24px;
}
`;
interface RequestTokenData {
requestData: {
@@ -99,20 +74,8 @@ export default function useUseageData<T>(config: {
defaultData?: T;
disabledDate?: boolean;
}) {
const { url, defaultData, disabledDate = false } = config || {};
const intl = useIntl();
const { TagRender } = useSelectRender({
maxTagWidth: 100,
filled: true,
style: {
height: 24,
lineHeight: '24px'
}
});
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({
range: DefaultDateConfig.maxRange,
disabledDate: disabledDate
});
const { url, defaultData } = config || {};
const [open, setOpen] = useState(false);
const [result, setResult] = useState<{
start_date?: string;
@@ -356,69 +319,6 @@ export default function useUseageData<T>(config: {
fetchUsersList();
};
const FilterBar = () => {
const filterOptions = (inputValue: any, option: any) => {
return option.label?.toLowerCase().includes(inputValue.toLowerCase());
};
return (
<FilterWrapper>
<div className="selection">
<DatePicker.RangePicker
maxDate={dayjs()}
defaultValue={[
dayjs().add(-DefaultDateConfig.defaultRange, 'd'),
dayjs()
]}
disabledDate={disabledRangeDaysDate}
presets={rangePresets}
allowClear={false}
style={{ width: 220 }}
value={[dayjs(query.start_date), dayjs(query.end_date)]}
onChange={handleDateChange}
></DatePicker.RangePicker>
<Select
allowClear
showSearch
mode="multiple"
options={userList}
maxTagCount={1}
filterOption={filterOptions}
tagRender={TagRender}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectuser'
})}
style={{ maxWidth: 200, minWidth: 160 }}
value={query.user_ids}
onChange={handleUsersChange}
></Select>
<Select
allowClear
showSearch
mode="multiple"
options={modelList}
maxTagCount={1}
tagRender={TagRender}
filterOption={filterOptions}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectmodel'
})}
value={query.model_ids}
style={{ maxWidth: 200, minWidth: 160 }}
onChange={handleModelsChange}
></Select>
{url === DASHBOARD_STATS_API && (
<Tooltip title={intl.formatMessage({ id: 'common.button.export' })}>
<Button
icon={<DownloadOutlined />}
onClick={handleExport}
></Button>
</Tooltip>
)}
</div>
</FilterWrapper>
);
};
return {
usageData,
result,
@@ -430,8 +330,10 @@ export default function useUseageData<T>(config: {
setQuery,
init,
setResult,
FilterBar,
handleOnCancel,
handleExport
handleExport,
handleDateChange,
handleUsersChange,
handleModelsChange
};
}
@@ -16,7 +16,7 @@ const SpinWrapper = styled.div`
justify-content: center;
top: 0;
left: 0;
height: 400px;
max-height: 400px;
right: 0;
`;
+2 -1
View File
@@ -551,7 +551,8 @@ export const useCheckCompatibility = () => {
const gpuSelector = generateGPUIds(data.values);
return await handleDoEvalute({
...data.values,
...gpuSelector
...gpuSelector,
replicas: allValues.replicas || 0
});
};