feat: display provider models

This commit is contained in:
jialin
2026-03-11 20:00:16 +08:00
parent 353ee3a9ae
commit 6f2c1daa41
9 changed files with 217 additions and 75 deletions
+31 -24
View File
@@ -3,6 +3,7 @@ import { isNotEmptyValue } from '@/utils/index';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import type { CascaderAutoProps } from 'antd'; import type { CascaderAutoProps } from 'antd';
import { Cascader, Empty, Form } from 'antd'; import { Cascader, Empty, Form } from 'antd';
import classNames from 'classnames';
import _, { cloneDeep } from 'lodash'; import _, { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import AutoTooltip from '../auto-tooltip'; import AutoTooltip from '../auto-tooltip';
@@ -10,28 +11,6 @@ import { SealFormItemProps } from './types';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select'; import SelectWrapper from './wrapper/select';
const tag = (props: any) => {
if (props.isMaxTag) {
return props.label?.slice(0, -3);
}
const parent = _.split(props.value, '__RC_CASCADER_SPLIT__')?.[0];
return `${parent} / ${props?.label}`;
};
const renderTag = (props: any) => {
return (
<AutoTooltip
closable={props.closable}
onClose={props.onClose}
maxWidth={240}
style={{ marginRight: 4 }}
filled
>
{tag(props)}
</AutoTooltip>
);
};
const OptionNodes = (props: { const OptionNodes = (props: {
data: any; data: any;
notFoundContent?: React.ReactNode; notFoundContent?: React.ReactNode;
@@ -100,7 +79,9 @@ const SealCascader: React.FC<
alwaysFocus = false, alwaysFocus = false,
optionNode, optionNode,
notFoundContent, notFoundContent,
size = 'middle',
tagRender, tagRender,
displayRender,
...rest ...rest
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
@@ -172,10 +153,34 @@ const SealCascader: React.FC<
props.onOpenChange?.(open); props.onOpenChange?.(open);
}; };
const tag = (props: any) => {
if (props.isMaxTag) {
return props.label?.slice(0, -3);
}
const parent = _.split(props.value, '__RC_CASCADER_SPLIT__')?.[0];
return displayRender ? props.label : `${parent} / ${props?.label}`;
};
const renderTag = (props: any) => {
return (
<AutoTooltip
closable={props.closable}
onClose={props.onClose}
maxWidth={240}
style={{ marginRight: 4 }}
filled
>
{tag(props)}
</AutoTooltip>
);
};
return ( return (
<SelectWrapper> <SelectWrapper>
<Wrapper <Wrapper
className="seal-select-wrapper" className={classNames('seal-select-wrapper', {
'seal-cascader-wrapper-small': size === 'small'
})}
classList={visible ? 'dropdown-visible' : ''} classList={visible ? 'dropdown-visible' : ''}
status={status} status={status}
label={label} label={label}
@@ -188,6 +193,7 @@ const SealCascader: React.FC<
<Cascader <Cascader
{...rest} {...rest}
placeholder={placeholder} placeholder={placeholder}
className={size === 'small' ? 'seal-cascader-small' : ''}
suffixIcon={<IconFont type="icon-down"></IconFont>} suffixIcon={<IconFont type="icon-down"></IconFont>}
optionRender={(data) => ( optionRender={(data) => (
<OptionNodes <OptionNodes
@@ -196,7 +202,8 @@ const SealCascader: React.FC<
optionNode={optionNode} optionNode={optionNode}
></OptionNodes> ></OptionNodes>
)} )}
tagRender={tagRender ?? renderTag} tagRender={tagRender || renderTag}
displayRender={displayRender}
ref={inputRef} ref={inputRef}
options={children ? null : _options} options={children ? null : _options}
onFocus={handleOnFocus} onFocus={handleOnFocus}
+2 -2
View File
@@ -179,8 +179,8 @@ const SimpleSelect: React.FC<SelectProps & { ref?: any; showTags?: boolean }> =
closable={props.closable} closable={props.closable}
onClose={props.onClose} onClose={props.onClose}
style={{ style={{
height: 24, height: 22,
backgroundColor: 'var(--ant-color-fill-tertiary)', backgroundColor: 'var(--ant-color-fill-secondary)',
fontSize: 'var(--ant-font-size)' fontSize: 'var(--ant-font-size)'
}} }}
className="flex-center" className="flex-center"
@@ -146,6 +146,9 @@ const SelectWrapper = styled.div`
margin-inline-start: 0 !important; margin-inline-start: 0 !important;
} }
} }
&.seal-cascader-small {
height: 40px;
}
.ant-select-input { .ant-select-input {
height: ${INPUTHEIGHT}px !important; height: ${INPUTHEIGHT}px !important;
@@ -190,6 +193,21 @@ const SelectWrapper = styled.div`
} }
} }
} }
&.seal-cascader-wrapper-small {
height: 40px;
.cascader-popup-wrapper {
top: 39px !important;
}
.ant-select-input {
height: 36px !important;
}
.ant-select {
padding-inline: 12px !important;
}
.__wrapper__.no-label .ant-select.ant-cascader .ant-select-placeholder {
top: 50% !important;
}
}
} }
`; `;
@@ -24,6 +24,7 @@ const ExportData: React.FC<{
result, result,
userList, userList,
modelList, modelList,
selectedModels,
query, query,
setQuery, setQuery,
handleExport, handleExport,
@@ -37,6 +38,21 @@ const ExportData: React.FC<{
disabledDate: false disabledDate: false
}); });
const getModelName = (record: any) => {
if (record.model_id) {
const children =
modelList.find((item) => item.value === 'deployments')?.children || [];
return (
children?.find((item) => item.value === record.model_id)?.label ||
record.model_id
);
}
const provider =
modelList.find((item) => item.value === record.provider_id)?.label ||
record.provider_id;
return `${provider} / ${record.model_name}`;
};
const exportTableColumns: TableColumnType[] = [ const exportTableColumns: TableColumnType[] = [
{ {
title: intl.formatMessage({ id: 'resources.table.index' }), title: intl.formatMessage({ id: 'resources.table.index' }),
@@ -63,12 +79,9 @@ const ExportData: React.FC<{
{ {
title: intl.formatMessage({ id: 'dashboard.usage.export.model' }), title: intl.formatMessage({ id: 'dashboard.usage.export.model' }),
dataIndex: 'model_id', dataIndex: 'model_id',
render: (text: string) => { render: (text: string, record: any) => {
return ( console.log('render model id: ', record, modelList);
<AutoTooltip ghost> return <AutoTooltip ghost>{getModelName(record)}</AutoTooltip>;
{modelList.find((item) => item.value === text)?.label || text}
</AutoTooltip>
);
} }
}, },
@@ -91,6 +104,8 @@ const ExportData: React.FC<{
width: 150 width: 150
} }
]; ];
console.log('export data: ', modelList);
const handleSubmit = () => { const handleSubmit = () => {
const fileName = `usage-data_${query.start_date || ''}_${query.end_date || ''}.xlsx`; const fileName = `usage-data_${query.start_date || ''}_${query.end_date || ''}.xlsx`;
exportJsonToExcel({ exportJsonToExcel({
@@ -111,8 +126,8 @@ const ExportData: React.FC<{
user_id: (value: string) => { user_id: (value: string) => {
return userList.find((item) => item.value === value)?.label || value; return userList.find((item) => item.value === value)?.label || value;
}, },
model_id: (value: string) => { model_id: (value: string, record: any) => {
return modelList.find((item) => item.value === value)?.label || value; return getModelName(record);
} }
} }
}); });
@@ -126,6 +141,7 @@ const ExportData: React.FC<{
start_date: dayjs().subtract(29, 'days').format('YYYY-MM-DD'), start_date: dayjs().subtract(29, 'days').format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'), end_date: dayjs().format('YYYY-MM-DD'),
model_ids: [], model_ids: [],
provider_model_names: [],
user_ids: [] user_ids: []
}); });
setResult({ setResult({
@@ -164,6 +180,8 @@ const ExportData: React.FC<{
query={query} query={query}
userList={userList} userList={userList}
modelList={modelList} modelList={modelList}
selectedModels={selectedModels}
cascaderWidth={360}
handleDateChange={handleDateChange} handleDateChange={handleDateChange}
handleUsersChange={handleUsersChange} handleUsersChange={handleUsersChange}
handleModelsChange={handleModelsChange} handleModelsChange={handleModelsChange}
@@ -171,7 +189,7 @@ const ExportData: React.FC<{
<Table <Table
columns={exportTableColumns} columns={exportTableColumns}
tableLayout={'auto'} tableLayout={'auto'}
style={{ width: '100%', marginTop: '16px' }} style={{ width: '100%', marginTop: '16px', minHeight: 300 }}
dataSource={result.data?.items || []} dataSource={result.data?.items || []}
loading={loading} loading={loading}
rowKey="id" rowKey="id"
@@ -1,4 +1,7 @@
import AutoTooltip from '@/components/auto-tooltip';
import SealCascader from '@/components/seal-form/seal-cascader';
import SimpleSelect from '@/components/seal-form/simple-select'; import SimpleSelect from '@/components/seal-form/simple-select';
import ProviderLogo from '@/pages/maas-provider/components/provider-logo';
import { DownloadOutlined } from '@ant-design/icons'; import { DownloadOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, DatePicker, Tooltip } from 'antd'; import { Button, DatePicker, Tooltip } from 'antd';
@@ -8,12 +11,26 @@ import styled from 'styled-components';
import { DASHBOARD_STATS_API } from '../../apis'; import { DASHBOARD_STATS_API } from '../../apis';
import useRangePickerPreset from '../../hooks/use-rangepicker-preset'; import useRangePickerPreset from '../../hooks/use-rangepicker-preset';
const OptionWrapper = styled.span`
display: flex;
align-items: center;
gap: 8px;
`;
const LabelWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;
const DefaultDateConfig = { const DefaultDateConfig = {
maxRange: 60, maxRange: 60,
defaultRange: 29 defaultRange: 29
}; };
const FilterWrapper = styled.div` const FilterWrapper = styled.div`
poisiton: relative;
z-index: 10;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -36,8 +53,10 @@ interface FilterBarProps {
handleUsersChange: (value: any) => void; handleUsersChange: (value: any) => void;
handleModelsChange: (value: any) => void; handleModelsChange: (value: any) => void;
handleExport?: () => void; handleExport?: () => void;
selectedModels: string[][];
url: string; url: string;
disabledDate?: boolean; disabledDate?: boolean;
cascaderWidth?: number;
} }
const FilterBar: React.FC<FilterBarProps> = (props) => { const FilterBar: React.FC<FilterBarProps> = (props) => {
@@ -45,6 +64,8 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
query, query,
userList, userList,
modelList, modelList,
selectedModels,
cascaderWidth = 300,
handleDateChange, handleDateChange,
handleUsersChange, handleUsersChange,
handleModelsChange, handleModelsChange,
@@ -52,7 +73,6 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
url, url,
disabledDate disabledDate
} = props; } = props;
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({ const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({
range: DefaultDateConfig.maxRange, range: DefaultDateConfig.maxRange,
disabledDate: disabledDate disabledDate: disabledDate
@@ -60,6 +80,50 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
const intl = useIntl(); const intl = useIntl();
const displayRender = (labels: any[], option: any) => {
return (
<AutoTooltip
ghost
maxWidth={150}
title={
<span>
{labels[0]} / {labels[1]}
</span>
}
>
{labels[0]} / {labels[1]}
</AutoTooltip>
);
};
const optionRender = (option: any) => {
const { data } = option;
if (!data.isParent) {
return <AutoTooltip ghost>{data.label}</AutoTooltip>;
}
if (data.providerType === 'deployments') {
return (
<OptionWrapper>
<ProviderLogo provider={data.providerType as string} />
<AutoTooltip ghost maxWidth={105}>
{intl.formatMessage({ id: 'menu.models.deployment' })}
</AutoTooltip>
</OptionWrapper>
);
}
return (
<OptionWrapper>
<ProviderLogo provider={data.providerType as string} />
<AutoTooltip ghost maxWidth={105}>
<span>{data.label}</span>
</AutoTooltip>
</OptionWrapper>
);
};
return ( return (
<FilterWrapper> <FilterWrapper>
<div className="selection"> <div className="selection">
@@ -89,19 +153,37 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
value={query.user_ids} value={query.user_ids}
onChange={handleUsersChange} onChange={handleUsersChange}
></SimpleSelect> ></SimpleSelect>
<SimpleSelect <SealCascader
allowClear
showSearch showSearch
mode="multiple" multiple={true}
options={modelList} onChange={handleModelsChange}
maxTagCount={0} classNames={{
popup: {
root: 'cascader-popup-wrapper gpu-selector'
}
}}
styles={{
root: {
width: cascaderWidth
},
popup: {
listItem: {
padding: '5px 10px'
}
}
}}
maxTagCount={1}
size="small"
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
id: 'dashboard.usage.selectmodel' id: 'dashboard.usage.selectmodel'
})} })}
value={query.model_ids} options={modelList}
style={{ maxWidth: 200, minWidth: 160 }} value={selectedModels}
onChange={handleModelsChange} showCheckedStrategy="SHOW_CHILD"
></SimpleSelect> displayRender={displayRender}
optionNode={optionRender}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader>
{url === DASHBOARD_STATS_API && ( {url === DASHBOARD_STATS_API && (
<Tooltip title={intl.formatMessage({ id: 'common.button.export' })}> <Tooltip title={intl.formatMessage({ id: 'common.button.export' })}>
<Button icon={<DownloadOutlined />} onClick={handleExport}></Button> <Button icon={<DownloadOutlined />} onClick={handleExport}></Button>
@@ -16,6 +16,7 @@ import useUsageData from './use-usage-data';
const TitleWrapper = styled.div` const TitleWrapper = styled.div`
margin: 0; margin: 0;
font-weight: 700; font-weight: 700;
min-width: max-content;
`; `;
const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => { const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
@@ -27,6 +28,7 @@ const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
query, query,
userList, userList,
modelList, modelList,
selectedModels,
handleOnCancel, handleOnCancel,
init, init,
handleExport, handleExport,
@@ -109,6 +111,7 @@ const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
query={query} query={query}
userList={userList} userList={userList}
modelList={modelList} modelList={modelList}
selectedModels={selectedModels}
disabledDate={true} disabledDate={true}
handleDateChange={handleDateChange} handleDateChange={handleDateChange}
handleUsersChange={handleUsersChange} handleUsersChange={handleUsersChange}
@@ -1,5 +1,6 @@
import { queryModelsList } from '@/pages/llmodels/apis'; import { queryModelsList } from '@/pages/llmodels/apis';
import { ListItem as ModelListItem } from '@/pages/llmodels/config/types'; import { ListItem as ModelListItem } from '@/pages/llmodels/config/types';
import useTargetSourceModels from '@/pages/model-routes/hooks/use-target-source-models';
import { queryUsersList } from '@/pages/users/apis'; import { queryUsersList } from '@/pages/users/apis';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
@@ -93,6 +94,7 @@ export default function useUseageData<T>(config: {
start_date: string; start_date: string;
end_date: string; end_date: string;
model_ids: number[]; model_ids: number[];
provider_model_names: string[];
user_ids: number[]; user_ids: number[];
}>({ }>({
start_date: dayjs() start_date: dayjs()
@@ -100,12 +102,15 @@ export default function useUseageData<T>(config: {
.format('YYYY-MM-DD'), .format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'), end_date: dayjs().format('YYYY-MM-DD'),
model_ids: [], model_ids: [],
user_ids: [] user_ids: [],
provider_model_names: []
}); });
const { sourceModels: modelList, fetchSourceModels } =
const [modelList, setModelList] = useState<Global.BaseOption<string>[]>([]); useTargetSourceModels();
const [models, setModelList] = useState<Global.BaseOption<string>[]>([]);
const [userList, setUserList] = useState<Global.BaseOption<string>[]>([]); const [userList, setUserList] = useState<Global.BaseOption<string>[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedModels, setSelectedModels] = useState<string[][]>([]);
const usageData = useMemo<{ const usageData = useMemo<{
requestTokenData: RequestTokenData; requestTokenData: RequestTokenData;
@@ -301,19 +306,36 @@ export default function useUseageData<T>(config: {
}); });
fetchUsageData({ ...query, user_ids: value }); fetchUsageData({ ...query, user_ids: value });
}; };
const handleModelsChange = (value: number[]) => {
const generateModelsValue = (value: string[][]) => {
const modelIds = [] as number[];
const providerModelNames = [] as string[];
value.forEach((item: Array<string | number>) => {
if (item[0] === 'deployments') {
modelIds.push(item[1] as number);
} else {
providerModelNames.push(`${item[0]}:${item[1]}`);
}
});
return {
model_ids: modelIds,
provider_model_names: providerModelNames
};
};
const handleModelsChange = (value: string[][]) => {
setSelectedModels(value);
setQuery((pre) => { setQuery((pre) => {
return { return {
...pre, ...pre,
model_ids: value ...generateModelsValue(value)
}; };
}); });
fetchUsageData({ ...query, model_ids: value }); fetchUsageData({ ...query, ...generateModelsValue(value) });
}; };
const init = () => { const init = () => {
fetchUsageData(query); fetchUsageData(query);
fetchModelsList(); fetchSourceModels();
fetchUsersList(); fetchUsersList();
}; };
@@ -325,6 +347,7 @@ export default function useUseageData<T>(config: {
userList, userList,
modelList, modelList,
query, query,
selectedModels,
setQuery, setQuery,
init, init,
setResult, setResult,
@@ -90,15 +90,8 @@ const RouteItem: React.FC<TargetItemProps> = ({
</Col> </Col>
<Col span={2}> <Col span={2}>
<CellContent> <CellContent>
{data.weight > 0 && (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight}
</AutoTooltip>
)}
{data.fallback_status_codes && {data.fallback_status_codes &&
data.fallback_status_codes?.length > 0 && ( data.fallback_status_codes?.length > 0 ? (
<> <>
{data.weight > 0 && ( {data.weight > 0 && (
<span style={{ marginInline: 8 }}>/</span> <span style={{ marginInline: 8 }}>/</span>
@@ -109,6 +102,11 @@ const RouteItem: React.FC<TargetItemProps> = ({
})} })}
</span> </span>
</> </>
) : (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight || 0}
</AutoTooltip>
)} )}
</CellContent> </CellContent>
</Col> </Col>
-7
View File
@@ -257,13 +257,6 @@ const TargetsForm = forwardRef((props, ref) => {
{ {
validator(rule, value) { validator(rule, value) {
if (value && value?.length > 0) { if (value && value?.length > 0) {
// if (_.some(value, (item: any) => !item.weight)) {
// setValidTriggered(true);
// return Promise.reject(
// getRuleMessage('input', 'routes.form.target.weight')
// );
// }
if ( if (
_.some( _.some(
dataList, dataList,