chore: access, endpoints to route targets
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user