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