From 9e29e7283c176db433193941ac242451e6a058be Mon Sep 17 00:00:00 2001 From: jialin Date: Tue, 1 Jul 2025 20:18:53 +0800 Subject: [PATCH] feat: add simple select component --- src/components/seal-form/simple-select.tsx | 147 ++++++++++++++++++ src/locales/en-US/common.ts | 5 +- src/locales/ja-JP/common.ts | 6 +- src/locales/ru-RU/common.ts | 4 +- src/locales/zh-CN/common.ts | 3 +- .../components/usage-inner/export-data.tsx | 19 ++- .../components/usage-inner/filter-bar.tsx | 121 ++++++++++++++ .../components/usage-inner/index.tsx | 45 ++++-- .../usage-inner/request-token-inner.tsx | 1 - .../components/usage-inner/use-usage-data.tsx | 112 +------------ .../llmodels/components/catalog-list.tsx | 2 +- src/pages/llmodels/hooks/index.ts | 3 +- 12 files changed, 339 insertions(+), 129 deletions(-) create mode 100644 src/components/seal-form/simple-select.tsx create mode 100644 src/pages/dashboard/components/usage-inner/filter-bar.tsx diff --git a/src/components/seal-form/simple-select.tsx b/src/components/seal-form/simple-select.tsx new file mode 100644 index 00000000..30ee9321 --- /dev/null +++ b/src/components/seal-form/simple-select.tsx @@ -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 = (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 ( + + {restProps.value?.includes(value) ? ( + + ) : ( + + )} + {label} + + ); + }; + + 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 ( + + {restProps.mode === 'multiple' && ( + + + {intl.formatMessage({ id: 'common.checbox.all' })} + + + )} + {originPanel} + + ); + }; + + 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 ( + + {intl.formatMessage({ id: 'common.select.count' }, { count: count })} + + ); + }; + + return ( + + ); +}; + +export default SimpleSelect; diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index eeb50f1f..cae944ee 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -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' }; diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index e809be28..cec55e7b 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -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 ========== diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index c7c2a312..7b124200 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -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 ========== diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 9b1be793..4fec1d53 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -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} 项' }; diff --git a/src/pages/dashboard/components/usage-inner/export-data.tsx b/src/pages/dashboard/components/usage-inner/export-data.tsx index c1147a9c..ef1ae0c3 100644 --- a/src/pages/dashboard/components/usage-inner/export-data.tsx +++ b/src/pages/dashboard/components/usage-inner/export-data.tsx @@ -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<{ > } > - + 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 = (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 ( + +
+ + + + {url === DASHBOARD_STATS_API && ( + + + + )} +
+
+ ); +}; + +export default FilterBar; diff --git a/src/pages/dashboard/components/usage-inner/index.tsx b/src/pages/dashboard/components/usage-inner/index.tsx index edca6cad..ef45cad3 100644 --- a/src/pages/dashboard/components/usage-inner/index.tsx +++ b/src/pages/dashboard/components/usage-inner/index.tsx @@ -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({ - 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({ + 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 }) => { {intl.formatMessage({ id: 'dashboard.usage' })} - + void; requestData: { name: string; color: string; diff --git a/src/pages/dashboard/components/usage-inner/use-usage-data.tsx b/src/pages/dashboard/components/usage-inner/use-usage-data.tsx index 55b58560..d2ab816a 100644 --- a/src/pages/dashboard/components/usage-inner/use-usage-data.tsx +++ b/src/pages/dashboard/components/usage-inner/use-usage-data.tsx @@ -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(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(config: { fetchUsersList(); }; - const FilterBar = () => { - const filterOptions = (inputValue: any, option: any) => { - return option.label?.toLowerCase().includes(inputValue.toLowerCase()); - }; - return ( - -
- - - - {url === DASHBOARD_STATS_API && ( - - - - )} -
-
- ); - }; - return { usageData, result, @@ -430,8 +330,10 @@ export default function useUseageData(config: { setQuery, init, setResult, - FilterBar, handleOnCancel, - handleExport + handleExport, + handleDateChange, + handleUsersChange, + handleModelsChange }; } diff --git a/src/pages/llmodels/components/catalog-list.tsx b/src/pages/llmodels/components/catalog-list.tsx index 91b1d11e..9da19f3e 100644 --- a/src/pages/llmodels/components/catalog-list.tsx +++ b/src/pages/llmodels/components/catalog-list.tsx @@ -16,7 +16,7 @@ const SpinWrapper = styled.div` justify-content: center; top: 0; left: 0; - height: 400px; + max-height: 400px; right: 0; `; diff --git a/src/pages/llmodels/hooks/index.ts b/src/pages/llmodels/hooks/index.ts index 64439ecc..94be9f77 100644 --- a/src/pages/llmodels/hooks/index.ts +++ b/src/pages/llmodels/hooks/index.ts @@ -551,7 +551,8 @@ export const useCheckCompatibility = () => { const gpuSelector = generateGPUIds(data.values); return await handleDoEvalute({ ...data.values, - ...gpuSelector + ...gpuSelector, + replicas: allValues.replicas || 0 }); };