feat: add simple select component
This commit is contained in:
@@ -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;
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
|
||||
@@ -551,7 +551,8 @@ export const useCheckCompatibility = () => {
|
||||
const gpuSelector = generateGPUIds(data.values);
|
||||
return await handleDoEvalute({
|
||||
...data.values,
|
||||
...gpuSelector
|
||||
...gpuSelector,
|
||||
replicas: allValues.replicas || 0
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user