chore: access, endpoints to route targets

This commit is contained in:
jialin
2026-02-03 18:21:08 +08:00
parent 6d65797b4f
commit 051d9421b0
36 changed files with 411 additions and 435 deletions
+76
View File
@@ -0,0 +1,76 @@
import { request } from '@umijs/max';
import { FormData, RouteItem, RouteTarget } from '../config/types';
export const MODEL_ROUTES = '/model-routes';
export const MODEL_ROUTE_TARGETS = '/model-route-targets';
export async function queryModelRoutes(
params: Global.SearchParams,
options?: any
) {
return request<Global.PageResponse<RouteItem>>(MODEL_ROUTES, {
params,
method: 'GET',
cancelToken: options?.token
});
}
export async function createModelRoute(params: { data: FormData }) {
return request(`${MODEL_ROUTES}`, {
method: 'POST',
data: params.data
});
}
export async function updateModelRoute(params: { id: number; data: FormData }) {
return request(`${MODEL_ROUTES}/${params.id}`, {
method: 'PUT',
data: params.data
});
}
export async function deleteModelRoute(id: number) {
return request(`${MODEL_ROUTES}/${id}`, {
method: 'DELETE'
});
}
export async function queryRouteTargets(params: { id: number }, options?: any) {
return request<Global.PageResponse<RouteTarget>>(
`${MODEL_ROUTE_TARGETS}?route_id=${params.id}`,
{
method: 'GET',
params: {
page: -1
},
cancelToken: options?.token
}
);
}
export async function deleteModelRouteTarget(id: number) {
return request(`${MODEL_ROUTE_TARGETS}/${id}`, {
method: 'DELETE'
});
}
export async function updateModelRouteTarget(params: {
id: number;
data: Partial<RouteTarget>;
}) {
return request(`${MODEL_ROUTE_TARGETS}/${params.id}`, {
method: 'PUT',
data: params.data
});
}
export async function setRouteTargetAsFallback(params: {
id: number;
data: Partial<RouteTarget>;
}) {
return request(`${MODEL_ROUTE_TARGETS}/${params.id}/set-fallback`, {
method: 'POST',
data: params.data
});
}
@@ -0,0 +1,59 @@
import { PageActionType } from '@/config/types';
import FormDrawer from '@/pages/_components/form-drawer';
import React, { useRef } from 'react';
import { FormData, RouteItem as ListItem } from '../config/types';
import ModelRouteForm from '../forms';
type AddModalProps = {
title: string;
action: PageActionType;
open: boolean;
currentData?: ListItem; // Used when action is EDIT
onOk: (values: FormData) => void;
onCancel: () => void;
};
const AddProvider: React.FC<AddModalProps> = ({
title,
action,
open,
currentData,
onOk,
onCancel
}) => {
const form = useRef<any>(null);
const handleSubmit = () => {
form.current?.submit();
};
const onFinish = async (data: FormData) => {
onOk({
...data
});
};
const handleCancel = () => {
form.current?.resetFields();
onCancel();
};
return (
<FormDrawer
title={title}
open={open}
onCancel={handleCancel}
onSubmit={handleSubmit}
width={600}
>
<ModelRouteForm
ref={form}
action={action}
currentData={currentData}
onFinish={onFinish}
/>
</FormDrawer>
);
};
export default AddProvider;
@@ -0,0 +1,50 @@
import FormDrawer from '@/pages/_components/form-drawer';
import React, { useRef } from 'react';
import { FormData, RouteItem as ListItem } from '../config/types';
import FallbackSettings from '../forms/fallback-settings';
type AddModalProps = {
title: string;
open: boolean;
currentData?: ListItem; // Used when action is EDIT
onOk: (values: FormData) => void;
onCancel: () => void;
};
const AddProvider: React.FC<AddModalProps> = ({
title,
open,
currentData,
onOk,
onCancel
}) => {
const form = useRef<any>(null);
const handleSubmit = () => {
form.current?.submit();
};
const onFinish = async (data: FormData) => {
onOk({
...data
});
};
const handleCancel = () => {
form.current?.resetFields();
onCancel();
};
return (
<FormDrawer
title={title}
open={open}
onCancel={handleCancel}
onSubmit={handleSubmit}
width={600}
>
<FallbackSettings currentData={currentData} onFinish={onFinish} />
</FormDrawer>
);
};
export default AddProvider;
@@ -0,0 +1,162 @@
import AutoTooltip from '@/components/auto-tooltip';
import DropdownButtons from '@/components/drop-down-buttons';
import IconFont from '@/components/icon-font';
import RowChildren from '@/components/seal-table/components/row-children';
import StatusTag from '@/components/status-tag';
import { DeleteOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Col, Row } from 'antd';
import dayjs from 'dayjs';
import React from 'react';
import styled from 'styled-components';
import { TargetStatus, TargetStatusValueMap } from '../config';
import { RouteTarget } from '../config/types';
const CellContent = styled.div`
display: flex;
align-items: center;
height: 100%;
`;
interface ProviderModelProps {
dataList: RouteTarget[];
onSelect: (val: any, record: any) => void;
sourceModels: any[];
}
interface AccessItemProps {
onSelect: (val: any, record: any) => void;
data: any;
sourceModels: any[];
}
export const childActionList = [
{
key: 'fallback',
label: 'routes.table.setAsFallback',
icon: <IconFont type="icon-shield" />
},
{
key: 'delete',
label: 'common.button.delete',
icon: <DeleteOutlined />,
props: {
danger: true
}
}
];
const RouteItem: React.FC<AccessItemProps> = ({
onSelect,
data,
sourceModels
}) => {
const intl = useIntl();
const renderProviderSource = () => {
const model = sourceModels.find((item: any) => {
if (data.model_id) {
return item.value === 'deployments';
}
return item.value === data.provider_id;
});
console.log('renderProviderSource model:', data, sourceModels);
return model?.label || '-';
};
return (
<div style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}>
<RowChildren>
<Row gutter={16} style={{ width: '100%' }}>
<Col span={5}>
<CellContent
style={{
paddingInline: 'var(--ant-table-cell-padding-inline)'
}}
>
<AutoTooltip ghost>{data.name}</AutoTooltip>
</CellContent>
</Col>
<Col span={4} style={{ paddingLeft: 56 }}>
<CellContent>{renderProviderSource()}</CellContent>
</Col>
<Col span={3}>
<CellContent>
{data.weight > 0 && (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight}
</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}>
<CellContent>
<AutoTooltip ghost>
<StatusTag
statusValue={{
status: TargetStatus[data.state],
text: TargetStatusValueMap[data.state],
message: ''
}}
/>
</AutoTooltip>
</CellContent>
</Col>
<Col span={5}>
<CellContent style={{ paddingLeft: 45 }}>
<AutoTooltip ghost minWidth={20}>
{dayjs(data.created_at).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip>
</CellContent>
</Col>
<Col span={4}>
<CellContent
style={{
paddingLeft: 38
}}
>
<DropdownButtons
items={childActionList}
onSelect={(val) => onSelect(val, data)}
></DropdownButtons>
</CellContent>
</Col>
</Row>
</RowChildren>
</div>
);
};
const RouteTargets: React.FC<ProviderModelProps> = ({
dataList,
onSelect,
sourceModels
}) => {
return (
<div>
{dataList.map((item, index) => (
<RouteItem
data={item}
key={index}
onSelect={onSelect}
sourceModels={sourceModels}
></RouteItem>
))}
</div>
);
};
export default RouteTargets;
+40
View File
@@ -0,0 +1,40 @@
import icons from '@/components/icon-font/icons';
import { StatusMaps } from '@/config';
import { StatusType } from '@/config/types';
export const TargetStatusValueMap: Record<string, string> = {
Active: 'active',
Unavailable: 'unavailable'
};
export const TargetStatusLabelMap = {
[TargetStatusValueMap.Active]: 'Active',
[TargetStatusValueMap.Unavailable]: 'Unavailable'
};
export const TargetStatus: Record<string, StatusType> = {
[TargetStatusValueMap.Active]: StatusMaps.success,
[TargetStatusValueMap.Unavailable]: StatusMaps.error
};
// actions for each row
export const rowActionList = [
{
key: 'edit',
label: 'common.button.edit',
icon: icons.EditOutlined
},
{
label: 'models.button.accessSettings',
key: 'accessControl',
icon: icons.Permission
},
{
key: 'delete',
label: 'common.button.delete',
icon: icons.DeleteOutlined,
props: {
danger: true
}
}
];
+50
View File
@@ -0,0 +1,50 @@
export interface FormData {
name: string;
description: string;
categories: any[];
meta: Record<string, any>;
generic_proxy: boolean;
fallback_target: {
provider_model_name?: string;
model_id?: number;
provider_id?: number;
fallback_status_codes?: string[];
};
targets: {
provider_model_name?: string;
weight?: number | null;
model_id?: number;
provider_id?: number;
fallback_status_codes?: string[];
}[];
}
export interface RouteItem {
id: number;
created_at: string;
updated_at: string;
deleted_at: string;
name: string;
description: string;
categories: string[];
meta: Record<string, any>;
created_by_model: boolean;
targets: number;
ready_targets: number;
access_policy: string;
}
export interface RouteTarget {
id: number;
created_at: string;
updated_at: string;
deleted_at: string;
provider_model_name: string;
weight: number | null;
model_id: number;
provider_id: number;
name: string;
route_id: number;
state: string;
fallback_status_codes: string[];
}
+67
View File
@@ -0,0 +1,67 @@
import CheckboxField from '@/components/seal-form/checkbox-field';
import SealInput from '@/components/seal-form/seal-input';
import useAppUtils from '@/hooks/use-app-utils';
import CategorySelect from '@/pages/_components/category-select';
import { categoryOptions } from '@/pages/llmodels/config';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { FormData } from '../config/types';
const Basic = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const { getRuleMessage } = useAppUtils();
return (
<>
<Form.Item name="name" data-field="name">
<SealInput.Input
required
label={intl.formatMessage({ id: 'models.table.name' })}
/>
</Form.Item>
<Form.Item
name="categories"
normalize={(value) => (value ? [value] : [])}
getValueProps={(value) => ({
value: Array.isArray(value) ? value[0] || null : value
})}
rules={[
{
required: true,
message: getRuleMessage('select', 'models.form.categories')
}
]}
>
<CategorySelect
required
options={categoryOptions}
label={intl.formatMessage({ id: 'models.form.categories' })}
></CategorySelect>
</Form.Item>
<Form.Item name="description" style={{ marginBottom: 8 }}>
<SealInput.TextArea
scaleSize={true}
label={intl.formatMessage({
id: 'common.table.description'
})}
></SealInput.TextArea>
</Form.Item>
<Form.Item<FormData>
name="generic_proxy"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
description={intl.formatMessage({
id: 'models.form.generic_proxy.tips'
})}
label={intl.formatMessage({
id: 'models.form.generic_proxy'
})}
></CheckboxField>
</Form.Item>
</>
);
};
export default Basic;
+206
View File
@@ -0,0 +1,206 @@
import IconFont from '@/components/icon-font';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import CollapsePanel from '@/pages/_components/collapse-panel';
import { useWrapperContext } from '@/pages/_components/column-wrapper/use-wrapper-context';
import ScrollSpyTabs from '@/pages/_components/scroll-spy-tabs';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import _ from 'lodash';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState
} from 'react';
import { FormData, RouteItem as ListItem } from '../config/types';
import useEditTargets from '../hooks/use-edit-targets';
import Basic from './basic';
import Targets from './targets';
interface ProviderFormProps {
ref?: any;
action: PageActionType;
currentData?: ListItem; // Used when action is EDIT
onFinish: (values: FormData) => Promise<void>;
}
const TABKeysMap = {
BASIC: 'basic',
METADATA: 'metadata',
TARGETS: 'targets',
ADVANCED: 'advanced'
};
const AccessForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const { action, currentData, onFinish } = props;
const intl = useIntl();
const { getScrollElementScrollableHeight } = useWrapperContext();
const [activeKey, setActiveKey] = useState<string[]>([TABKeysMap.BASIC]);
const [form] = Form.useForm();
const scrollTabsRef = useRef<any>(null);
const targetsRef = useRef<any>(null);
const { generateTargetData, fetchTargets } = useEditTargets();
const segmentOptions = [
{
value: TABKeysMap.BASIC,
label: intl.formatMessage({ id: 'common.title.basicInfo' }),
icon: <IconFont type="icon-basic" />,
field: 'name'
},
{
value: TABKeysMap.TARGETS,
label: intl.formatMessage({ id: 'routes.form.target.title' }),
icon: <IconFont type="icon-language" />,
field: 'targets'
}
];
const handleActiveChange = (key: string[]) => {
setActiveKey(key);
};
const formatTargets = (values: FormData) => {
let targetList = [...(values.targets || [])];
let fallbackTarget = values.fallback_target;
if (fallbackTarget) {
const exsitinged = targetList.find((ep) => {
if (fallbackTarget!.model_id) {
return ep.model_id === fallbackTarget!.model_id;
}
return (
ep.provider_id === fallbackTarget!.provider_id &&
ep.provider_model_name === fallbackTarget!.provider_model_name
);
});
if (exsitinged) {
targetList = targetList.map((ep) => {
if (
ep.model_id === fallbackTarget.model_id ||
ep.provider_model_name === fallbackTarget.provider_model_name
) {
return {
...ep,
fallback_status_codes: ['4xx', '5xx']
};
}
return ep;
});
}
if (!exsitinged) {
targetList.push({
...fallbackTarget,
weight: 0,
fallback_status_codes: ['4xx', '5xx']
});
}
}
return targetList;
};
const handleOnFinish = (values: FormData) => {
const targets = formatTargets(values);
const data = {
..._.omit(values, ['targets', 'fallback_target']),
targets: targets
};
console.log('data=========', data);
onFinish(data);
};
const handleOnCollapseChange = (keys: string | string[]) => {
setActiveKey(Array.isArray(keys) ? keys : [keys]);
};
useEffect(() => {
const initEditionForm = async () => {
const targetList = await fetchTargets(currentData!.id);
const { targets, fallbackTarget } = generateTargetData(targetList);
// init form values
form.setFieldsValue({
...currentData,
targets: targets,
fallback_target: fallbackTarget
});
// init targets form list
targetsRef.current?.initDataList(
targets?.map((ep) => ({
weight: ep.weight,
value: ep.model_id
? ['deployments', ep.model_id]
: [ep.provider_id, ep.provider_model_name]
})) || []
);
// init fallback value
if (fallbackTarget) {
targetsRef.current?.initFallbackValues({
value: fallbackTarget.model_id
? ['deployments', fallbackTarget.model_id]
: [fallbackTarget.provider_id, fallbackTarget.provider_model_name]
});
}
};
if (action === PageAction.EDIT && currentData) {
initEditionForm();
} else {
form.resetFields();
}
}, [action, currentData, form]);
useImperativeHandle(ref, () => ({
submit: () => {
form.submit();
},
resetFields: () => {
form.resetFields();
}
}));
return (
<ScrollSpyTabs
ref={scrollTabsRef}
defaultTarget="basic"
segmentOptions={segmentOptions}
activeKey={activeKey}
setActiveKey={handleActiveChange}
segmentedTop={{
top: 0,
offsetTop: 96
}}
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
>
<Form
form={form}
onFinish={handleOnFinish}
initialValues={{
categories: [],
meta: {}
}}
>
<Basic />
<CollapsePanel
activeKey={activeKey}
accordion={false}
onChange={handleOnCollapseChange}
items={[
{
key: TABKeysMap.TARGETS,
label: intl.formatMessage({ id: 'routes.form.target.title' }),
forceRender: true,
children: <Targets ref={targetsRef}></Targets>
}
]}
></CollapsePanel>
</Form>
</ScrollSpyTabs>
);
});
export default AccessForm;
+176
View File
@@ -0,0 +1,176 @@
import SingleImage from '@/components/auto-image/single-image';
import ListInput from '@/components/list-input';
import SealInputNumber from '@/components/seal-form/input-number';
import SealInput from '@/components/seal-form/seal-input';
import UploadImg from '@/pages/playground/components/upload-img';
import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd';
import styled from 'styled-components';
import { FormData } from '../config/types';
const SizeWrapper = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 16px;
`;
const UploadWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
`;
const IconBox = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
padding: 14px;
border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius);
.icon {
color: var(--ant-color-text-tertiary);
font-size: 14px;
line-height: 1;
}
`;
const MetaData = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const icon = Form.useWatch(['meta', 'icon'], form);
const handleMetadataChange = (list: string[], field: string) => {
form.setFieldValue(['meta', field], list);
};
const handleUpdateImageList = (fileList: any[]) => {
if (fileList.length === 0) {
return;
}
form.setFieldValue(['meta', 'icon'], fileList[0]?.dataUrl || null);
};
const handleDeleteIcon = () => {
form.setFieldValue(['meta', 'icon'], null);
};
return (
<>
<SizeWrapper>
<Form.Item<FormData>
data-field="metaSize"
name={['meta', 'size']}
normalize={(v) => (v === 0 ? null : v)}
>
<SealInputNumber
min={0}
label={`${intl.formatMessage({ id: 'routes.form.metadata.size' })} (B)`}
></SealInputNumber>
</Form.Item>
<Form.Item<FormData>
name={['meta', 'activated_size']}
normalize={(v) => (v === 0 ? null : v)}
>
<SealInputNumber
min={0}
label={`${intl.formatMessage({
id: 'routes.form.metadata.activeSize'
})} (B)`}
></SealInputNumber>
</Form.Item>
</SizeWrapper>
<SizeWrapper>
<Form.Item<FormData> name={['meta', 'max_tokens']}>
<SealInput.Input
label={intl.formatMessage({
id: 'routes.form.metadata.maxTokens'
})}
placeholder={intl.formatMessage(
{ id: 'common.help.eg' },
{ content: 'context/128k' }
)}
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name={['meta', 'dimensions']}
normalize={(v) => (v === 0 ? null : v)}
>
<SealInputNumber
min={0}
step={1}
precision={0}
label={intl.formatMessage({
id: 'routes.form.metadata.dimension'
})}
></SealInputNumber>
</Form.Item>
</SizeWrapper>
<Form.Item<FormData> name={['meta', 'release_date']}>
<SealInput.Input
label={intl.formatMessage({
id: 'routes.form.metadata.releaseDate'
})}
placeholder={intl.formatMessage(
{ id: 'common.help.eg' },
{ content: '2025-05-19' }
)}
></SealInput.Input>
</Form.Item>
<Form.Item<FormData> name={['meta', 'tags']} data-field="metadata">
<ListInput
label={intl.formatMessage({ id: 'resources.form.label' })}
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
dataList={[]}
onChange={(list: string[]) => handleMetadataChange(list, 'tags')}
></ListInput>
</Form.Item>
<Form.Item<FormData> name={['meta', 'licenses']} data-field="metadata">
<ListInput
label={intl.formatMessage({ id: 'routes.form.metadata.license' })}
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
dataList={[]}
onChange={(list: string[]) => handleMetadataChange(list, 'licenses')}
></ListInput>
</Form.Item>
<Form.Item<FormData> name={['meta', 'languages']} data-field="metadata">
<ListInput
label={intl.formatMessage({ id: 'routes.form.metadata.languages' })}
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
dataList={[]}
onChange={(list: string[]) => handleMetadataChange(list, 'languages')}
></ListInput>
</Form.Item>
<Form.Item<FormData> name={['meta', 'icon']} data-field="metadata">
<IconBox>
<span className="icon">
{intl.formatMessage({ id: 'routes.form.metadata.icons' })}
</span>
<UploadWrapper>
<UploadImg
handleUpdateImgList={handleUpdateImageList}
size="middle"
>
{icon ? (
<SingleImage
uid={1}
editable={true}
onDelete={handleDeleteIcon}
dataUrl={icon}
preview={false}
/>
) : (
<Button size="middle" variant="dashed" color="default">
{intl.formatMessage({
id: 'routes.form.metadata.uploadIcon'
})}
</Button>
)}
</UploadImg>
</UploadWrapper>
</IconBox>
</Form.Item>
</>
);
};
export default MetaData;
+245
View File
@@ -0,0 +1,245 @@
import MetadataList from '@/components/metadata-list';
import SealCascader from '@/components/seal-form/seal-cascader';
import SealInput from '@/components/seal-form/seal-input';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import _ from 'lodash';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import styled from 'styled-components';
import { FormData } from '../config/types';
import useTargetSourceModels from '../hooks/use-target-source-models';
const Inner = styled.div`
display: flex;
align-items: center;
width: 100%;
ul.ant-cascader-menu:first-child {
li[data-path-key='deployments'] {
position: relative;
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
border-bottom: 1px solid var(--ant-color-split);
}
}
}
`;
const TargetsForm = forwardRef((props, ref) => {
const intl = useIntl();
const { sourceModels, loading, fetchSourceModels } = useTargetSourceModels();
const form = Form.useFormInstance<FormData>();
const targets = Form.useWatch('targets', form) || [];
const [fallbackValues, setFallbackValues] = useState<{ value: any[] }>({
value: []
});
const [dataList, setDataList] = useState<
{
weight: number | null;
value: any[];
}[]
>([]);
useImperativeHandle(ref, () => ({
initFallbackValues: (values: { value: any[] }) => {
setFallbackValues(values);
},
initDataList: (
list: {
weight: number | null;
value: any[];
}[]
) => {
setDataList(list);
}
}));
const handleTargetsChange = (value: any[], index: number, options: any[]) => {
const selectedOption =
options?.find?.((opt) => opt.value === value[1]) || {};
const targetList = [...targets];
targetList[index] = {
weight: targetList[index]?.weight || null,
...selectedOption?.data
};
form.setFieldValue('targets', [...targetList]);
const newDataList = [...dataList];
newDataList[index] = {
weight: newDataList[index]?.weight || null,
value: value
};
setDataList(newDataList);
};
const handleOnAdd = () => {
const newDataList = [
...dataList,
{
weight: null,
value: []
}
];
setDataList(newDataList);
};
const handleOnDelete = (index: number, item: any) => {
const newDataList = dataList.filter((_, i) => i !== index);
setDataList(newDataList);
const targetList = [...targets];
targetList.splice(index, 1);
form.setFieldValue('targets', [...targetList]);
};
const handleFallbackChange = (value: any[], options?: any[]) => {
const selectedOption =
options?.find?.((opt) => opt.value === value[1]) || {};
console.log('fallback selected option data:', value);
form.setFieldValue('fallback_target', {
...selectedOption?.data
});
setFallbackValues({
value: value
});
};
const handleOnWeightChange = (value: any, index: number) => {
const targetList = [...targets];
if (targetList[index]) {
targetList[index] = {
...targetList[index],
weight: value
};
form.setFieldValue('targets', [...targetList]);
}
const newDataList = [...dataList];
newDataList[index] = {
...newDataList[index],
weight: value
};
setDataList(newDataList);
};
useEffect(() => {
fetchSourceModels();
}, []);
return (
<>
<Form.Item
name="targets"
data-field="targets"
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (_.keys(value).length > 0) {
if (_.some(_.keys(value), (k: string) => !value[k])) {
return Promise.reject(
intl.formatMessage(
{
id: 'common.validate.value'
},
{
name: intl.formatMessage({
id: 'models.form.selector'
})
}
)
);
}
}
return Promise.resolve();
}
})
]}
>
<MetadataList
label={''}
styles={{
wrapper: {
paddingTop: 14
}
}}
dataList={dataList}
btnText={intl.formatMessage({ id: 'routes.form.target.add' })}
onAdd={handleOnAdd}
onDelete={handleOnDelete}
>
{(item, index) => (
<>
<SealCascader
required
showSearch
expandTrigger="hover"
multiple={false}
alwaysFocus={true}
onChange={(value, options) =>
handleTargetsChange(value, index, options)
}
classNames={{
popup: {
root: 'cascader-popup-wrapper gpu-selector'
}
}}
maxTagCount={1}
placeholder={intl.formatMessage({
id: 'providers.form.target.placeholder'
})}
value={item.value}
options={sourceModels}
showCheckedStrategy="SHOW_CHILD"
getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader>
<span className="seprator">:</span>
<SealInput.Number
style={{ flex: 100 }}
min={0}
value={item.weight}
onChange={(value) => handleOnWeightChange(value, index)}
placeholder={intl.formatMessage({
id: 'routes.form.target.weight'
})}
></SealInput.Number>
</>
)}
</MetadataList>
</Form.Item>
<Form.Item name="fallback_target">
<div>
<SealCascader
showSearch
expandTrigger="hover"
multiple={false}
alwaysFocus={true}
classNames={{
popup: {
root: 'cascader-popup-wrapper gpu-selector'
}
}}
label={intl.formatMessage({
id: 'routes.form.target.fallback'
})}
placeholder={intl.formatMessage({
id: 'providers.form.target.placeholder'
})}
maxTagCount={1}
value={fallbackValues.value}
options={sourceModels}
onChange={(value, options) => handleFallbackChange(value, options)}
showCheckedStrategy="SHOW_CHILD"
getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader>
</div>
</Form.Item>
</>
);
});
export default TargetsForm;
@@ -0,0 +1,49 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { useState } from 'react';
import { RouteItem as ListItem } from '../config/types';
const useAccessControl = () => {
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
currentData?: ListItem;
title: string;
}>({
open: false,
action: PageAction.CREATE,
currentData: undefined,
title: ''
});
const openModal = (
action: PageActionType,
title: string,
currentData?: ListItem
) => {
setOpenModalStatus({
open: true,
action,
currentData,
title: title
});
};
const closeModal = () => {
setOpenModalStatus({
open: false,
action: PageAction.CREATE,
currentData: undefined,
title: ''
});
};
return {
openAccessControlModalStatus: openModalStatus,
setOpenAccessControlModalStatus: setOpenModalStatus,
openAccessControlModal: openModal,
closeAccessControlModal: closeModal
};
};
export default useAccessControl;
@@ -0,0 +1,49 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { useState } from 'react';
import { RouteItem as ListItem } from '../config/types';
const useCreateRoute = (options?: { refresh: () => void }) => {
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
currentData?: ListItem;
title: string;
}>({
open: false,
action: PageAction.CREATE,
currentData: undefined,
title: ''
});
const openModal = (
action: PageActionType,
title: string,
currentData?: ListItem
) => {
setOpenModalStatus({
open: true,
action,
currentData,
title: title
});
};
const closeModal = () => {
setOpenModalStatus({
open: false,
action: PageAction.CREATE,
currentData: undefined,
title: ''
});
};
return {
openRouteModalStatus: openModalStatus,
setOpenRouteModalStatus: setOpenModalStatus,
openRouteModal: openModal,
closeRouteModal: closeModal
};
};
export default useCreateRoute;
@@ -0,0 +1,37 @@
import { queryRouteTargets } from '../apis';
import { RouteTarget } from '../config/types';
const useEditTargets = () => {
const generateTargetData = (targetList: RouteTarget[]) => {
const fallbackTarget =
targetList?.filter(
(ep) => ep.fallback_status_codes && ep.fallback_status_codes?.length > 0
)?.[0] || null;
const targets = targetList?.filter(
(ep) =>
!ep.fallback_status_codes || ep.fallback_status_codes?.length === 0
);
return {
targets: targets,
fallbackTarget: fallbackTarget
};
};
const fetchTargets = async (accessId: number) => {
try {
const res = await queryRouteTargets({ id: accessId });
return res.items || [];
} catch (error) {
return [];
}
};
return {
generateTargetData,
fetchTargets
};
};
export default useEditTargets;
@@ -0,0 +1,71 @@
// columns.ts
import AutoTooltip from '@/components/auto-tooltip';
import DropdownButtons from '@/components/drop-down-buttons';
import { SealColumnProps } from '@/components/seal-table/types';
import { tableSorter } from '@/config/settings';
import ModelTag from '@/pages/_components/model-tag';
import { useIntl } from '@umijs/max';
import dayjs from 'dayjs';
import { useMemo } from 'react';
import { rowActionList } from '../config';
import { RouteItem } from '../config/types';
const useAccessColumns = (
handleSelect: (val: string, record: RouteItem) => void,
onCellClick?: (record: RouteItem, dataIndex: string) => void
): SealColumnProps[] => {
const intl = useIntl();
return useMemo(() => {
return [
{
title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: 'name',
sorter: tableSorter(1),
span: 5,
render: (text: string, record: RouteItem) => (
<span className="flex-center" style={{ maxWidth: '100%' }}>
<AutoTooltip ghost title={text}>
<span className="m-r-5">{text}</span>
</AutoTooltip>
<ModelTag categoryKey={record.categories?.[0]}></ModelTag>
</span>
)
},
{
title: intl.formatMessage({ id: 'routes.table.routeTargets' }),
dataIndex: 'targets',
span: 10,
render: (value: number, record: RouteItem) => (
<span>
{record.ready_targets} / {value}
</span>
)
},
{
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: 'created_at',
sorter: tableSorter(6),
span: 5,
render: (value: string) => (
<AutoTooltip ghost minWidth={20}>
{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.operation' }),
dataIndex: 'operations',
span: 4,
render: (value: string, record: RouteItem) => (
<DropdownButtons
items={rowActionList}
onSelect={(val) => handleSelect(val, record)}
></DropdownButtons>
)
}
];
}, [handleSelect, onCellClick]);
};
export default useAccessColumns;
@@ -0,0 +1,91 @@
import { queryModelsList } from '@/pages/llmodels/apis';
import { ListItem as ModelListItem } from '@/pages/llmodels/config/types';
import { queryMaasProviders } from '@/pages/maas-provider/apis';
import { MaasProviderItem } from '@/pages/maas-provider/config/types';
import { useIntl } from '@umijs/max';
import { useState } from 'react';
type EmptyObject = Record<never, never>;
type CascaderOption<T extends object = EmptyObject> = {
label: string;
value: string | number;
parent?: boolean;
disabled?: boolean;
index?: number;
children?: CascaderOption<T>[];
} & Partial<T>;
const useTargetSourceModels = () => {
const intl = useIntl();
const [sourceModels, setSourceModels] = useState<CascaderOption[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const fetchSourceModels = async (params?: any) => {
setLoading(true);
try {
const [models, providers] = await Promise.all([
queryModelsList({ page: -1, ...params }),
queryMaasProviders({ page: -1, ...params })
]);
const modelsList = [
{
label: (
<span>
{intl.formatMessage({ id: 'menu.models.deployment' })}
<span
style={{
color: 'var(--ant-color-text-tertiary)',
marginLeft: 4
}}
>
[GPUStack]
</span>
</span>
),
value: 'deployments',
parent: true,
children: models.items?.map?.((model: ModelListItem) => ({
label: model.name,
value: model.id,
data: {
model_id: model.id
},
source: 'deployment'
}))
}
].filter((group) => group.children && group.children.length > 0);
const providerOptions: CascaderOption[] = providers.items
?.map?.((provider: MaasProviderItem) => ({
label: provider.name,
value: provider.id,
parent: true,
children: provider.models?.map?.((model) => ({
label: model.name,
value: model.name,
data: {
provider_model_name: model.name,
provider_id: provider.id
},
source: 'providerModel'
}))
}))
.filter((group) => group.children && group.children.length > 0);
setSourceModels([...modelsList, ...providerOptions]);
} catch (error) {
setSourceModels([]);
} finally {
setLoading(false);
}
};
return {
sourceModels,
loading,
fetchSourceModels
};
};
export default useTargetSourceModels;
+300
View File
@@ -0,0 +1,300 @@
import { expandKeysAtom } from '@/atoms/clusters';
import DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools';
import SealTable from '@/components/seal-table';
import TableContext from '@/components/seal-table/table-context';
import { TableOrder } from '@/components/seal-table/types';
import { PageAction } from '@/config';
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
import useTableFetch from '@/hooks/use-table-fetch';
import useWatchList from '@/hooks/use-watch-list';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { message } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash';
import { useEffect } from 'react';
import NoResult from '../_components/no-result';
import PageBox from '../_components/page-box';
import AccessControlModal from '../llmodels/components/access-control-modal';
import {
MODEL_ROUTES,
MODEL_ROUTE_TARGETS,
createModelRoute,
deleteModelRoute,
deleteModelRouteTarget,
queryModelRoutes,
queryRouteTargets,
setRouteTargetAsFallback,
updateModelRoute
} from './apis';
import AddRouteModal from './components/add-route-modal';
import RouteTargets from './components/route-targets';
import { FormData, RouteItem as ListItem } from './config/types';
import useAccessControl from './hooks/use-access-control';
import useCreateRoute from './hooks/use-create-route';
import useRoutesColumns from './hooks/use-routes-columns';
import useTargetSourceModels from './hooks/use-target-source-models';
const Accesses: React.FC = () => {
const {
dataSource,
rowSelection,
queryParams,
modalRef,
handleTableChange,
handleDelete,
handleDeleteBatch,
fetchData,
handlePageChange,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
fetchAPI: queryModelRoutes,
deleteAPI: deleteModelRoute,
watch: true,
API: MODEL_ROUTES,
contentForDelete: 'menu.models.routes'
});
const { watchDataList: allRouteTargets, deleteItemFromCache } =
useWatchList(MODEL_ROUTE_TARGETS);
const [expandAtom] = useAtom(expandKeysAtom);
const { handleExpandChange, handleExpandAll, expandedRowKeys } =
useExpandedRowKeys(expandAtom);
const intl = useIntl();
const { openRouteModalStatus, openRouteModal, closeRouteModal } =
useCreateRoute();
const {
openAccessControlModal,
closeAccessControlModal,
openAccessControlModalStatus
} = useAccessControl();
const { sourceModels, fetchSourceModels } = useTargetSourceModels();
const handleClickDropdown = () => {
openRouteModal(
PageAction.CREATE,
intl.formatMessage({ id: 'routes.button.add' })
);
};
const handleModalOk = async (data: FormData) => {
const params = {
...data
};
try {
if (openRouteModalStatus.action === PageAction.EDIT) {
await updateModelRoute({
data: params,
id: openRouteModalStatus.currentData!.id
});
}
if (openRouteModalStatus.action === PageAction.CREATE) {
await createModelRoute({
data: params
});
}
fetchData();
closeRouteModal();
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {}
};
const handleModalCancel = () => {
console.log('handleModalCancel');
closeRouteModal();
};
const handleEditProvider = (row: ListItem) => {
openRouteModal(
PageAction.EDIT,
intl.formatMessage({ id: 'common.button.edit.item' }, { name: row.name }),
row
);
};
const handleSelect = useMemoizedFn((val: any, row: ListItem) => {
if (val === 'edit') {
handleEditProvider(row);
} else if (val === 'delete') {
handleDelete({ ...row, name: row.name });
} else if (val === 'accessControl') {
openAccessControlModal(
PageAction.EDIT,
intl.formatMessage({ id: 'models.button.accessSettings' }),
row
);
}
});
const handleOnToggleExpandAll = () => {
// do nothing
};
const handleToggleExpandAll = useMemoizedFn((expanded: boolean) => {
const keys = dataSource.dataList?.map((item) => item.id);
handleExpandAll(expanded, keys);
if (expanded) {
handleOnToggleExpandAll();
}
});
const loadChildrenData = useMemoizedFn(
async (row: ListItem, options?: any) => {
const params = {
id: row.id
};
const res = await queryRouteTargets(params, {
token: options?.token
});
return res.items || [];
}
);
const handleOnSortChange = (order: TableOrder | Array<TableOrder>) => {
handleTableChange({}, {}, order, { action: 'sort' });
};
const handleDeleteTarget = (row: any) => {
modalRef.current?.show({
content: 'routes.table.routeTargets',
okText: 'common.button.delete',
operation: 'common.delete.single.confirm',
name: row.name,
async onOk() {
await deleteModelRouteTarget(row.id);
deleteItemFromCache?.(row.id);
}
});
};
const onChildSelect = useMemoizedFn(async (val: any, record: any) => {
try {
if (val === 'fallback') {
await setRouteTargetAsFallback({
id: record.id,
data: {
fallback_status_codes: ['4xx', '5xx']
}
});
message.success(intl.formatMessage({ id: 'common.message.success' }));
} else if (val === 'delete') {
handleDeleteTarget(record);
}
} catch (error) {}
});
const renderChildren = (
list: any,
options: { parent?: any; [key: string]: any }
) => {
return (
<RouteTargets
dataList={list}
onSelect={onChildSelect}
sourceModels={sourceModels}
/>
);
};
const setDisableExpand = (record: any) => {
return !record?.targets;
};
useEffect(() => {
fetchSourceModels();
}, []);
const columns = useRoutesColumns(handleSelect);
return (
<>
<PageBox>
<FilterBar
showSelect={false}
marginBottom={22}
marginTop={30}
widths={{ input: 300 }}
buttonText={intl.formatMessage({ id: 'routes.button.add' })}
rowSelection={rowSelection}
handleInputChange={handleNameChange}
handleSearch={handleSearch}
handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleClickDropdown}
></FilterBar>
<TableContext.Provider
value={{
allChildren: allRouteTargets,
setDisableExpand: setDisableExpand
}}
>
<SealTable
rowKey="id"
loadChildren={loadChildrenData}
sortDirections={TABLE_SORT_DIRECTIONS}
expandedRowKeys={expandedRowKeys}
onExpand={handleExpandChange}
onExpandAll={handleToggleExpandAll}
renderChildren={renderChildren}
onTableSort={handleOnSortChange}
showSorterTooltip={false}
dataSource={dataSource.dataList}
loading={dataSource.loading}
loadend={dataSource.loadend}
rowSelection={rowSelection}
columns={columns}
childParentKey="access_id"
expandable={true}
empty={
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-extension-outline" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText={intl.formatMessage({
id: 'noresult.accesses.nofound'
})}
title={intl.formatMessage({ id: 'noresult.accesses.title' })}
subTitle={intl.formatMessage({
id: 'noresult.accesses.subTitle'
})}
onClick={handleClickDropdown}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
></NoResult>
}
pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
></SealTable>
</TableContext.Provider>
</PageBox>
<AddRouteModal
open={openRouteModalStatus.open}
action={openRouteModalStatus.action}
title={openRouteModalStatus.title}
currentData={openRouteModalStatus.currentData}
onCancel={handleModalCancel}
onOk={handleModalOk}
></AddRouteModal>
<AccessControlModal
onCancel={closeAccessControlModal}
title={openAccessControlModalStatus.title}
open={openAccessControlModalStatus.open}
currentData={openAccessControlModalStatus.currentData || null}
action={openAccessControlModalStatus.action}
></AccessControlModal>
<DeleteModal ref={modalRef}></DeleteModal>
</>
);
};
export default Accesses;