Skip to content

Commit e8d4ebb

Browse files
authored
feat: add plugin display support for AI routes (higress-group#608)
1 parent 799f785 commit e8d4ebb

File tree

3 files changed

+150
-19
lines changed

3 files changed

+150
-19
lines changed

frontend/src/pages/ai/route.tsx

Lines changed: 129 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import i18n from "@/i18n";
12
import { AiRoute, AiUpstream } from '@/interfaces/ai-route';
2-
import { RoutePredicate } from '@/interfaces/route';
3+
import { fetchPluginsByRoute, RoutePredicate } from '@/interfaces/route';
4+
import { WasmPluginData } from '@/interfaces/wasm-plugin';
5+
import { getI18nValue } from "@/pages/plugin/utils";
36
import { addAiRoute, deleteAiRoute, getAiRoutes, updateAiRoute } from '@/services/ai-route';
7+
import { getWasmPlugins } from '@/services';
48
import { ArrowRightOutlined, ExclamationCircleOutlined, RedoOutlined, SearchOutlined } from '@ant-design/icons';
59
import { PageContainer } from '@ant-design/pro-layout';
610
import { useRequest } from 'ahooks';
7-
import { Button, Col, Drawer, Form, FormProps, Input, Modal, Row, Space, Table } from 'antd';
11+
import { Button, Col, Drawer, Form, FormProps, Input, message, Modal, Row, Space, Table } from 'antd';
812
import { history } from 'ice';
913
import React, { useEffect, useRef, useState } from 'react';
1014
import { Trans, useTranslation } from 'react-i18next';
@@ -33,7 +37,14 @@ const AiRouteList: React.FC = () => {
3337
if (!Array.isArray(value) || !value.length) {
3438
return '-';
3539
}
36-
return value.map((token) => <span>{token}</span>).reduce((prev, curr) => [prev, <br />, curr]);
40+
const elements: React.ReactNode[] = [];
41+
value.forEach((token, index) => {
42+
if (index > 0) {
43+
elements.push(<br key={`br-${index}`} />);
44+
}
45+
elements.push(<span key={`span-${index}`}>{token}</span>);
46+
});
47+
return elements;
3748
},
3849
},
3950
{
@@ -78,15 +89,23 @@ const AiRouteList: React.FC = () => {
7889
elements.push(`${upstream.provider}: ${upstream.weight}%`);
7990
});
8091
}
81-
if (record.fallbackUpstream?.provider) {
92+
if (record.fallbackConfig?.enabled && record.fallbackConfig.upstreams && record.fallbackConfig.upstreams.length > 0) {
93+
const fallbackProviders = record.fallbackConfig.upstreams.map(u => u.provider).join(', ');
8294
elements.push((
8395
<>
8496
<ArrowRightOutlined style={{ marginRight: '5px' }} />
85-
{record.fallbackUpstream.provider}
97+
{fallbackProviders}
8698
</>
87-
))
99+
));
88100
}
89-
return elements.map((ele) => <span>{ele}</span>).reduce((prev, curr) => [prev, <br />, curr]);
101+
const result: React.ReactNode[] = [];
102+
elements.forEach((ele, index) => {
103+
if (index > 0) {
104+
result.push(<br key={`br-${index}`} />);
105+
}
106+
result.push(<span key={`span-${index}`}>{ele}</span>);
107+
});
108+
return result;
90109
},
91110
},
92111
{
@@ -96,20 +115,27 @@ const AiRouteList: React.FC = () => {
96115
render: (value, record) => {
97116
const { authConfig } = record;
98117
if (!authConfig || !authConfig.enabled) {
99-
return t('aiRoute.authNotEnabled')
118+
return t('aiRoute.authNotEnabled');
100119
}
101120
if (!Array.isArray(value) || !value.length) {
102-
return t('aiRoute.authEnabledWithoutConsumer')
121+
return t('aiRoute.authEnabledWithoutConsumer');
103122
}
104-
return value.map((consumer) => <span>{consumer}</span>).reduce((prev, curr) => [prev, <br />, curr]);
123+
const result: React.ReactNode[] = [];
124+
value.forEach((consumer, index) => {
125+
if (index > 0) {
126+
result.push(<br key={`br-${index}`} />);
127+
}
128+
result.push(<span key={`span-${index}`}>{consumer}</span>);
129+
});
130+
return result;
105131
},
106132
},
107133
{
108134
title: t('misc.actions'),
109135
dataIndex: 'action',
110136
key: 'action',
111137
width: 240,
112-
align: 'center',
138+
align: 'center' as const,
113139
render: (_, record) => (
114140
<Space size="small">
115141
<a onClick={() => onUsageDrawer(record)}>{t('aiRoute.usage')}</a>
@@ -132,7 +158,20 @@ const AiRouteList: React.FC = () => {
132158
const [isLoading, setIsLoading] = useState<boolean>(false);
133159
const [confirmLoading, setConfirmLoading] = useState(false);
134160
const [usageDrawer, setUsageDrawer] = useState(false)
135-
const [usageCommand, setUsageCommand] = useState('')
161+
const [usageCommand, setUsageCommand] = useState<string>('')
162+
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
163+
const [pluginData, setPluginsData] = useState<Record<string, WasmPluginData[]>>({});
164+
const [pluginInfoList, setPluginInfoList] = useState<WasmPluginData[]>([]);
165+
166+
const { loading: wasmLoading, run: loadWasmPlugins } = useRequest(() => {
167+
return getWasmPlugins(i18n.language)
168+
}, {
169+
manual: true,
170+
onSuccess: (result = []) => {
171+
let plugins = result || [];
172+
setPluginInfoList(plugins);
173+
},
174+
});
136175

137176
const { loading, run, refresh } = useRequest(getAiRoutes, {
138177
manual: true,
@@ -149,6 +188,14 @@ const AiRouteList: React.FC = () => {
149188

150189
useEffect(() => {
151190
run();
191+
loadWasmPlugins();
192+
193+
const handleLanguageChange = () => loadWasmPlugins();
194+
i18n.on('languageChanged', handleLanguageChange);
195+
196+
return () => {
197+
i18n.off('languageChanged', handleLanguageChange);
198+
};
152199
}, []);
153200

154201
const buildUsageCommand = (aiRoute: AiRoute): string => {
@@ -179,7 +226,7 @@ const AiRouteList: React.FC = () => {
179226
};
180227

181228
const closeUsage = () => {
182-
setUsageCommand(null);
229+
setUsageCommand('');
183230
setUsageDrawer(false);
184231
}
185232

@@ -258,6 +305,9 @@ const AiRouteList: React.FC = () => {
258305
};
259306

260307
const handleModalOk = async () => {
308+
if (!currentAiRoute) {
309+
return;
310+
}
261311
setConfirmLoading(true);
262312
try {
263313
await deleteAiRoute(currentAiRoute.name);
@@ -279,6 +329,35 @@ const AiRouteList: React.FC = () => {
279329
setCurrentAiRoute(null);
280330
};
281331

332+
const onShowStrategyList = async (record: AiRoute, expanded: boolean) => {
333+
if (expanded) {
334+
try {
335+
const routeResourceName = `ai-route-${record.name}.internal`;
336+
const plugins = await fetchPluginsByRoute({ name: routeResourceName } as any);
337+
const mergedPlugins = plugins.map((plugin) => {
338+
const pluginInfo = pluginInfoList.find(
339+
info => info.name === plugin.name && !plugin.internal,
340+
);
341+
return {
342+
...plugin,
343+
title: pluginInfo?.title || plugin.title || '',
344+
description: pluginInfo?.description || plugin.description || '',
345+
};
346+
})
347+
setPluginsData((prev) => ({
348+
...prev,
349+
[record.name]: mergedPlugins,
350+
}));
351+
} catch (error) {
352+
message.error('Failed to fetch strategies, error:', error);
353+
setExpandedKeys((prev) => prev.filter((key) => key !== record.name));
354+
}
355+
} else {
356+
setExpandedKeys((prev) =>
357+
prev.filter((key) => key !== record.name));
358+
}
359+
};
360+
282361
return (
283362
<PageContainer>
284363
<Form
@@ -320,6 +399,43 @@ const AiRouteList: React.FC = () => {
320399
dataSource={dataSource}
321400
columns={columns}
322401
pagination={false}
402+
expandable={{
403+
expandedRowKeys: expandedKeys,
404+
onExpand: async (expanded, record) => {
405+
if (expanded) {
406+
setExpandedKeys([...expandedKeys, record.name]);
407+
} else {
408+
setExpandedKeys(expandedKeys.filter(key => key !== record.name));
409+
}
410+
await onShowStrategyList(record, expanded);
411+
},
412+
expandedRowRender: (record) => {
413+
const plugins = (pluginData[record.name] || []).filter(plugin => plugin.enabled);
414+
return (
415+
<Table
416+
dataSource={plugins}
417+
columns={[
418+
{
419+
title: t('plugins.title'),
420+
render: (_, plugin) => {
421+
return getI18nValue(plugin, 'title');
422+
},
423+
key: 'title',
424+
},
425+
{
426+
title: t('plugins.description'),
427+
render: (_, plugin) => {
428+
return getI18nValue(plugin, 'description');
429+
},
430+
key: 'description',
431+
},
432+
]}
433+
pagination={false}
434+
rowKey={(plugin) => `${plugin.name}-${plugin.internal}`}
435+
/>
436+
);
437+
},
438+
}}
323439
/>
324440
<Drawer
325441
title={t(currentAiRoute ? "aiRoute.edit" : "aiRoute.create")}

frontend/src/pages/plugin/components/PluginList/index.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ const PluginList = forwardRef((props: Props, ref) => {
9292
};
9393
});
9494
}
95+
} else if (type === QueryType.AI_ROUTE) {
96+
const aiRouteName = searchParams.get('name');
97+
if (aiRouteName) {
98+
const routeResourceName = `ai-route-${aiRouteName}.internal`;
99+
const pluginByRoutes = await fetchPluginsByRoute({ name: routeResourceName } as any);
100+
plugins = result.map((plugin: { name: string }) => {
101+
const foundPlugin = pluginByRoutes.find((p) => p.name === plugin.name);
102+
return {
103+
...plugin,
104+
enabled: foundPlugin ? foundPlugin.enabled : false,
105+
};
106+
});
107+
}
95108
}
96109
setPluginList(plugins);
97110
},
@@ -158,7 +171,7 @@ const PluginList = forwardRef((props: Props, ref) => {
158171
// Render a single plugin card
159172
const renderPluginItem = (item: WasmPluginData) => {
160173
const key = item.key || `${item.name}:${item.imageVersion}`;
161-
const showTag = type === QueryType.ROUTE || type === QueryType.DOMAIN;
174+
const showTag = type === QueryType.ROUTE || type === QueryType.DOMAIN || type === QueryType.AI_ROUTE;
162175
return (
163176
<Col span={6} key={key} xl={6} lg={12} md={12} sm={12} xs={24}>
164177
<Card

frontend/src/pages/route/index.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,14 @@ const RouteList: React.FC = () => {
173173
useEffect(() => {
174174
run({});
175175
loadWasmPlugins();
176-
}, []);
177176

178-
i18n.on('languageChanged', () => loadWasmPlugins());
177+
const handleLanguageChange = () => loadWasmPlugins();
178+
i18n.on('languageChanged', handleLanguageChange);
179+
180+
return () => {
181+
i18n.off('languageChanged', handleLanguageChange);
182+
};
183+
}, []);
179184

180185
const onEditDrawer = (route: Route) => {
181186
setCurrentRoute(route);
@@ -280,9 +285,6 @@ const RouteList: React.FC = () => {
280285
...prev,
281286
[record.name]: mergedPlugins,
282287
}));
283-
if (plugins.some(plugin => plugin.enabled)) {
284-
setExpandedKeys((prev) => [...prev, record.name]);
285-
}
286288
} catch (error) {
287289
message.error('Failed to fetch strategies, error:', error);
288290
setExpandedKeys((prev) => prev.filter((key) => key !== record.name));

0 commit comments

Comments
 (0)