diff --git a/backend/console/src/main/java/com/alibaba/higress/console/controller/WasmPluginInstancesController.java b/backend/console/src/main/java/com/alibaba/higress/console/controller/WasmPluginInstancesController.java index 6c6865ea..24677e74 100644 --- a/backend/console/src/main/java/com/alibaba/higress/console/controller/WasmPluginInstancesController.java +++ b/backend/console/src/main/java/com/alibaba/higress/console/controller/WasmPluginInstancesController.java @@ -32,7 +32,6 @@ import com.alibaba.higress.console.controller.dto.PaginatedResponse; import com.alibaba.higress.console.controller.dto.Response; import com.alibaba.higress.console.controller.util.ControllerUtil; -import com.alibaba.higress.sdk.constant.HigressConstants; import com.alibaba.higress.sdk.exception.ValidationException; import com.alibaba.higress.sdk.model.PaginatedResult; import com.alibaba.higress.sdk.model.Service; @@ -44,7 +43,6 @@ import com.alibaba.higress.sdk.service.ServiceService; import com.alibaba.higress.sdk.service.WasmPluginInstanceService; import com.alibaba.higress.sdk.service.WasmPluginService; -import com.alibaba.higress.sdk.service.kubernetes.KubernetesUtil; /** * @author CH3CHO @@ -154,18 +152,12 @@ public ResponseEntity> addOrUpdateRouteInstance( @PathVariable("routeName") @NotBlank String routeName, @PathVariable("name") @NotBlank String pluginName, @RequestBody WasmPluginInstance instance) { validateRouteName(routeName); - if (routeName.endsWith(HigressConstants.INTERNAL_RESOURCE_NAME_SUFFIX)) { - throw new ValidationException("Changing Wasm plugin configuration of an internal route is not allowed."); - } return addOrUpdateInstance(WasmPluginInstanceScope.ROUTE, routeName, pluginName, instance); } @DeleteMapping(value = "/routes/{routeName}/plugin-instances/{name}") public void deleteRouteInstance(@PathVariable("routeName") @NotBlank String routeName, @PathVariable("name") @NotBlank String pluginName) { - if (routeName.endsWith(HigressConstants.INTERNAL_RESOURCE_NAME_SUFFIX)) { - throw new ValidationException("Changing Wasm plugin configuration of an internal route is not allowed."); - } deleteInstance(WasmPluginInstanceScope.ROUTE, routeName, pluginName); } @@ -188,18 +180,12 @@ public ResponseEntity> addOrUpdateServiceInstance( @PathVariable("serviceName") @NotBlank String serviceName, @PathVariable("name") @NotBlank String pluginName, @RequestBody WasmPluginInstance instance) { validateServiceName(serviceName); - if (KubernetesUtil.isInternalService(serviceName)) { - throw new ValidationException("Changing Wasm plugin configuration of an internal service is not allowed."); - } return addOrUpdateInstance(WasmPluginInstanceScope.SERVICE, serviceName, pluginName, instance); } @DeleteMapping(value = "/services/{serviceName}/plugin-instances/{name}") public void deleteServiceInstance(@PathVariable("serviceName") @NotBlank String serviceName, @PathVariable("name") @NotBlank String pluginName) { - if (serviceName.endsWith(HigressConstants.INTERNAL_RESOURCE_NAME_SUFFIX)) { - throw new ValidationException("Changing Wasm plugin configuration of an internal service is not allowed."); - } deleteInstance(WasmPluginInstanceScope.SERVICE, serviceName, pluginName); } diff --git a/backend/console/src/test/java/com/alibaba/higress/console/FrontEndI18nResourceChecker.java b/backend/console/src/test/java/com/alibaba/higress/console/FrontEndI18nResourceChecker.java new file mode 100644 index 00000000..452429d2 --- /dev/null +++ b/backend/console/src/test/java/com/alibaba/higress/console/FrontEndI18nResourceChecker.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2022-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.alibaba.higress.console; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + +@Disabled +public class FrontEndI18nResourceChecker { + + private static final String FRONTEND_PROJECT_PATH = ""; + private static final String I18N_RESOURCE_PATH = "src/locales"; + private static final String I18N_RESOURCE_FILE_NAME = "translation.json"; + private static final List TS_FILE_EXTENSIONS = Arrays.asList(".ts", ".tsx"); + + private static final Set IMPLICITLY_USED_RESOURCE_KEYS = Set.of("init.title", "login.title", "aiRoute.edit", + "tlsCertificate.editTlsCertificate", "serviceSource.editServiceSource", "llmProvider.edit", + "plugins.editPlugin", "route.editRoute", "domain.editDomain", "consumer.edit"); + private static final List IMPLICITLY_USED_RESOURCE_KEY_PREFIXES = + Arrays.asList("menu.", "request.error.", "serviceSource.types.", "llmProvider.providerTypes.", + "route.factorGroup.required.", "route.keyValueGroup.required.", "plugins.configForm.", "plugins.subTitle."); + + private static final String LANG_CN = "zh-CN"; + private static final String LANG_EN = "en-US"; + + private static final List RESOURCE_USAGE_PATTERNS = Arrays.asList(Pattern.compile("\\bt\\('([^']+)'\\)"), + Pattern.compile("t\\(\"([^\"]+)\"\\)"), Pattern.compile("\\bi18nKey=\"([^\"]+)\"")); + private static final Pattern BAD_RESOURCE_CONTENT = Pattern.compile("^[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)+$"); + + @Test + public void checkI18nResourceUsages() throws IOException { + Map cnResources = loadI18nResources(LANG_CN); + Set usedResources = new TreeSet<>(); + Set badResources = new TreeSet<>(); + Set unknownResources = new TreeSet<>(); + try (Stream stream = Files.walk(Paths.get(FRONTEND_PROJECT_PATH))) { + stream.filter(p -> { + String s = p.toString(); + return !s.contains("node_modules") && TS_FILE_EXTENSIONS.stream().anyMatch(s::endsWith); + }).forEach(p -> { + String content; + try { + content = Files.readString(p, StandardCharsets.UTF_8); + } catch (IOException e) { + System.out.println("Failed to read file: " + p); + return; + } + Set referredResources = getReferredResources(content); + usedResources.addAll(referredResources); + referredResources.forEach(k -> { + if (!cnResources.containsKey(k)) { + unknownResources.add(k); + } + }); + }); + } + System.out.println("Unused resources:"); + for (String key : cnResources.keySet()) { + if (IMPLICITLY_USED_RESOURCE_KEYS.contains(key)) { + continue; + } + if (IMPLICITLY_USED_RESOURCE_KEY_PREFIXES.stream().anyMatch(key::startsWith)) { + continue; + } + if (!usedResources.contains(key)) { + System.out.println(key); + } + String value = cnResources.get(key); + if (BAD_RESOURCE_CONTENT.matcher(value).matches()) { + badResources.add(key); + } + } + System.out.println("-------------------------------------"); + System.out.println("Bad resources:"); + for (String key : badResources) { + System.out.printf("\"%s\": \"%s\",\n", key, cnResources.get(key)); + } + System.out.println("-------------------------------------"); + System.out.println("Unknown resources:"); + for (String key : unknownResources) { + String lastSegment = key.substring(key.lastIndexOf('.') + 1); + System.out.printf("\"%s\": \"%s\",\n", lastSegment, key); + } + } + + @Test + public void checkI18nResourcesAlignment() throws IOException { + Map cnResources = loadI18nResources(LANG_CN); + Map enResources = loadI18nResources(LANG_EN); + Set commonKeys = new HashSet<>(cnResources.keySet()); + commonKeys.retainAll(enResources.keySet()); + commonKeys.forEach(k -> { + cnResources.remove(k); + enResources.remove(k); + }); + System.out.println("Chinese resources without English translation:"); + for (String key : cnResources.keySet()) { + System.out.println(key); + } + System.out.println(); + System.out.println("English resources without Chinese translation:"); + for (String key : enResources.keySet()) { + System.out.println(key); + } + } + + @Test + public void checkUntranslatedEnglishResources() throws IOException { + Map enResources = loadI18nResources(LANG_EN); + for (Map.Entry entry : enResources.entrySet()) { + if (hasChinese(entry.getValue())) { + System.out.println(entry.getKey()); + } + } + } + + private boolean hasChinese(String str) { + for (int i = 0, len = str.length(); i < len; ++i) { + char ch = str.charAt(i); + if (Character.UnicodeScript.of(ch) == Character.UnicodeScript.HAN) { + return true; + } + } + return false; + } + + private static Set getReferredResources(String content) { + Set result = new HashSet<>(); + for (Pattern pattern : RESOURCE_USAGE_PATTERNS) { + Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + String key = matcher.group(1); + result.add(key); + } + } + return result; + } + + private static Map loadI18nResources(String language) throws IOException { + Path resourceFilePath = Paths.get(FRONTEND_PROJECT_PATH, I18N_RESOURCE_PATH, language, I18N_RESOURCE_FILE_NAME); + String json = Files.readString(resourceFilePath, StandardCharsets.UTF_8); + JSONObject obj = JSON.parseObject(json); + Map resources = new LinkedHashMap<>(); + addI18nResources(resources, obj, ""); + return resources; + } + + private static void addI18nResources(Map resources, JSONObject obj, String prefix) { + for (Map.Entry entry : obj.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof JSONObject) { + addI18nResources(resources, (JSONObject)value, prefix + key + "."); + } else { + resources.put(prefix + key, value.toString()); + } + } + } +} diff --git a/backend/sdk/src/main/java/com/alibaba/higress/sdk/model/TlsCertificate.java b/backend/sdk/src/main/java/com/alibaba/higress/sdk/model/TlsCertificate.java index f9a94b76..160090e9 100644 --- a/backend/sdk/src/main/java/com/alibaba/higress/sdk/model/TlsCertificate.java +++ b/backend/sdk/src/main/java/com/alibaba/higress/sdk/model/TlsCertificate.java @@ -39,9 +39,9 @@ public class TlsCertificate implements VersionedDto { private List domains; - @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss") + @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss'Z'") private LocalDateTime validityStart; - @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss") + @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss'Z'") private LocalDateTime validityEnd; } diff --git a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/DomainServiceImpl.java b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/DomainServiceImpl.java index d92747bc..ee564eee 100644 --- a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/DomainServiceImpl.java +++ b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/DomainServiceImpl.java @@ -67,6 +67,10 @@ public Domain add(Domain domain) { } throw new BusinessException("Error occurs when adding a new domain.", e); } + + // Sync the domain configs of routes using the new domain, especially the default domain + syncRouteDomainConfigs(domain.getName()); + return kubernetesModelConverter.configMap2Domain(newDomainConfigMap); } @@ -129,20 +133,26 @@ public Domain put(Domain domain) { "Error occurs when replacing the ConfigMap generated by domain: " + domain.getName(), e); } + // Sync the domain configs of routes using the domain + syncRouteDomainConfigs(domain.getName()); + + return kubernetesModelConverter.configMap2Domain(updatedConfigMap); + } + + private void syncRouteDomainConfigs(String domainName) { List routes; - if (HigressConstants.DEFAULT_DOMAIN.equals(domain.getName())) { + if (HigressConstants.DEFAULT_DOMAIN.equals(domainName)) { PaginatedResult routeQueryResult = routeService.list(null); routes = routeQueryResult.getData().stream().filter(r -> CollectionUtils.isEmpty(r.getDomains())) .collect(Collectors.toList()); } else { RoutePageQuery query = new RoutePageQuery(); - query.setDomainName(domain.getName()); + query.setDomainName(domainName); PaginatedResult routeQueryResult = routeService.list(query); routes = routeQueryResult.getData(); } // TODO: Switch to the new logic after 2025/03/31 - // String domainName = domain.getName(); // if (HigressConstants.DEFAULT_DOMAIN.equals(domainName)) { // domainName = HigressConstants.DEFAULT_DOMAIN; // } @@ -154,7 +164,5 @@ public Domain put(Domain domain) { if (CollectionUtils.isNotEmpty(routes)) { routes.forEach(routeService::update); } - - return kubernetesModelConverter.configMap2Domain(updatedConfigMap); } } \ No newline at end of file diff --git a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/consumer/ConsumerServiceImpl.java b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/consumer/ConsumerServiceImpl.java index bef28b19..56194a99 100644 --- a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/consumer/ConsumerServiceImpl.java +++ b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/consumer/ConsumerServiceImpl.java @@ -60,6 +60,7 @@ public Consumer addOrUpdate(Consumer consumer) { instance.setInternal(true); instance.setGlobalTarget(); } + instance.setEnabled(true); if (config.saveConsumer(instance, consumer)) { instancesToUpdate.add(instance); } diff --git a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/kubernetes/KubernetesModelConverter.java b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/kubernetes/KubernetesModelConverter.java index 9679100d..244022cb 100644 --- a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/kubernetes/KubernetesModelConverter.java +++ b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/kubernetes/KubernetesModelConverter.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -1735,7 +1736,7 @@ private static List getCertBoundDomains(String certData) { } private static List getCertBoundDomains(X509Certificate certificate) { - List domains = new ArrayList<>(); + Set domains = new LinkedHashSet<>(); String subjectDomain = getPrincipleValue(certificate.getSubjectX500Principal(), "CN"); if (StringUtils.isNotEmpty(subjectDomain)) { @@ -1764,7 +1765,7 @@ private static List getCertBoundDomains(X509Certificate certificate) { } } - return domains; + return new ArrayList<>(domains); } private static String getPrincipleValue(X500Principal principal, String type) { diff --git a/frontend/src/components/CardAreaChart/index.module.css b/frontend/src/components/CardAreaChart/index.module.css deleted file mode 100644 index 4362150c..00000000 --- a/frontend/src/components/CardAreaChart/index.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.cardSubTitle { - color: var(--color-text1-2, #999); - font-size: 12px; - line-height: 22px; - letter-spacing: 0; -} - -.cardDes { - margin-top: 6px; - color: var(--color-text1-2, #999); - font-size: 12px; - line-height: 17px; - letter-spacing: 0; -} - -.cardDes span { - margin-left: 2px; - color: #36cfc9; - font-weight: bold; -} - -.cardValue { - margin-top: 10px; - color: #333; - font-weight: var(--font-weight-3, bold); - font-size: 28px; - line-height: 28px; - letter-spacing: 0; -} - -.cardFoot { - padding: 13px 16px; - color: #666; - font-size: 12px; -} diff --git a/frontend/src/components/CardAreaChart/index.tsx b/frontend/src/components/CardAreaChart/index.tsx deleted file mode 100644 index 3a27a5ff..00000000 --- a/frontend/src/components/CardAreaChart/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { TinyArea } from '@ant-design/charts'; -import { Card } from 'antd'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import styles from './index.module.css'; -import mock from './mock'; - -interface CardConfig { - title: string | React.ReactNode; - subTitle: string | React.ReactNode; - value: string; - chartData: number[]; - des: string; - rate: string; - chartHeight: number; -} - -const DEFAULT_DATA: CardConfig = { - title: '', - subTitle: 'chart.area.defaultData.subTitle', - value: mock.value, - chartData: mock.saleList, - des: 'chart.area.defaultData.des', - rate: '12.0', - chartHeight: 100, -}; - -interface CardAreaChartProps { - cardConfig?: CardConfig; -} - -const CardAreaChart: React.FunctionComponent = (props): JSX.Element => { - const { t } = useTranslation(); - - const { - cardConfig = DEFAULT_DATA, - } = props; - const { title, subTitle, value, chartData, des, rate, chartHeight } = cardConfig; - - return ( - -
{typeof subTitle === 'string' ? t(subTitle) : subTitle}
-
{value}
-
{t(des)}{rate}↑
- -
- ); -}; - -export default CardAreaChart; diff --git a/frontend/src/components/CardAreaChart/mock.ts b/frontend/src/components/CardAreaChart/mock.ts deleted file mode 100644 index dc099c1b..00000000 --- a/frontend/src/components/CardAreaChart/mock.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - value: '123,456', - saleList: [3, 9, 5, 8, 11, 6, 8, 7], - dailySale: '¥1,234', -}; diff --git a/frontend/src/components/CardBarChart/index.module.css b/frontend/src/components/CardBarChart/index.module.css deleted file mode 100644 index 4362150c..00000000 --- a/frontend/src/components/CardBarChart/index.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.cardSubTitle { - color: var(--color-text1-2, #999); - font-size: 12px; - line-height: 22px; - letter-spacing: 0; -} - -.cardDes { - margin-top: 6px; - color: var(--color-text1-2, #999); - font-size: 12px; - line-height: 17px; - letter-spacing: 0; -} - -.cardDes span { - margin-left: 2px; - color: #36cfc9; - font-weight: bold; -} - -.cardValue { - margin-top: 10px; - color: #333; - font-weight: var(--font-weight-3, bold); - font-size: 28px; - line-height: 28px; - letter-spacing: 0; -} - -.cardFoot { - padding: 13px 16px; - color: #666; - font-size: 12px; -} diff --git a/frontend/src/components/CardBarChart/index.tsx b/frontend/src/components/CardBarChart/index.tsx deleted file mode 100644 index b5d3e0fb..00000000 --- a/frontend/src/components/CardBarChart/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { TinyColumn } from '@ant-design/charts'; -import { Card } from 'antd'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import styles from './index.module.css'; -import mock from './mock'; - -interface CardConfig { - title?: string | React.ReactNode; - subTitle?: string | React.ReactNode; - value?: string; - chartData?: number[]; - des?: string; - rate?: number; - chartHeight?: number; -} - -const DEFAULT_DATA: CardConfig = { - subTitle: 'chart.bar.defaultData.subTitle', - value: mock.value, - chartData: mock.saleList, - des: 'chart.bar.defaultData.des', - rate: 10.1, - chartHeight: 100, -}; - -export interface CardBarChartProps { - cardConfig?: CardConfig; -} - -const CardBarChart: React.FunctionComponent = (props: CardBarChartProps): JSX.Element => { - const { t } = useTranslation(); - - const { - cardConfig = DEFAULT_DATA, - } = props; - - const { title, subTitle, value, chartData, des, rate, chartHeight } = cardConfig; - - return ( - -
{typeof subTitle === 'string' ? t(subTitle) : subTitle}
-
{value}
-
{typeof des === 'string' ? t(des) : des}{rate}↑
- -
- ); -}; - -export default CardBarChart; diff --git a/frontend/src/components/CardBarChart/mock.ts b/frontend/src/components/CardBarChart/mock.ts deleted file mode 100644 index dc099c1b..00000000 --- a/frontend/src/components/CardBarChart/mock.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - value: '123,456', - saleList: [3, 9, 5, 8, 11, 6, 8, 7], - dailySale: '¥1,234', -}; diff --git a/frontend/src/components/CardGroupBarChart/index.module.css b/frontend/src/components/CardGroupBarChart/index.module.css deleted file mode 100644 index 57fa4bae..00000000 --- a/frontend/src/components/CardGroupBarChart/index.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.cardGroupBarChart { - height: 100%; -} \ No newline at end of file diff --git a/frontend/src/components/CardGroupBarChart/index.tsx b/frontend/src/components/CardGroupBarChart/index.tsx deleted file mode 100644 index cf61f8f9..00000000 --- a/frontend/src/components/CardGroupBarChart/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Column } from '@ant-design/charts'; -import { Card } from 'antd'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import styles from './index.module.css'; - -interface CardConfig { - title?: string; - chartData?: Array>; - chartHeight?: number; -} - -const DEFAULT_DATA: CardConfig = { - title: 'chart.groupBar.defaultData.title', - chartData: [ - { category: 'chart.groupBar.defaultData.category_1', value: 123, type: 'chart.groupBar.defaultData.shop_1' }, - { category: 'chart.groupBar.defaultData.category_1', value: 231, type: 'chart.groupBar.defaultData.shop_2' }, - { category: 'chart.groupBar.defaultData.category_1', value: 321, type: 'chart.groupBar.defaultData.shop_3' }, - { category: 'chart.groupBar.defaultData.category_2', value: -234, type: 'chart.groupBar.defaultData.shop_1' }, - { category: 'chart.groupBar.defaultData.category_2', value: -342, type: 'chart.groupBar.defaultData.shop_2' }, - { category: 'chart.groupBar.defaultData.category_2', value: -432, type: 'chart.groupBar.defaultData.shop_3' }, - { category: 'chart.groupBar.defaultData.category_3', value: 322, type: 'chart.groupBar.defaultData.shop_1' }, - { category: 'chart.groupBar.defaultData.category_3', value: 211, type: 'chart.groupBar.defaultData.shop_2' }, - { category: 'chart.groupBar.defaultData.category_3', value: 113, type: 'chart.groupBar.defaultData.shop_3' }, - { category: 'chart.groupBar.defaultData.category_4', value: 435, type: 'chart.groupBar.defaultData.shop_1' }, - { category: 'chart.groupBar.defaultData.category_4', value: 543, type: 'chart.groupBar.defaultData.shop_2' }, - { category: 'chart.groupBar.defaultData.category_4', value: 333, type: 'chart.groupBar.defaultData.shop_3' }, - { category: 'chart.groupBar.defaultData.category_5', value: 111, type: 'chart.groupBar.defaultData.shop_1' }, - { category: 'chart.groupBar.defaultData.category_5', value: 452, type: 'chart.groupBar.defaultData.shop_2' }, - { category: 'chart.groupBar.defaultData.category_5', value: 234, type: 'chart.groupBar.defaultData.shop_3' }, - ], - chartHeight: 500, -}; - -export interface CardGroupBarChartProps { - cardConfig?: CardConfig; -} - -const CardGroupBarChart: React.FunctionComponent = (props: CardGroupBarChartProps): JSX.Element => { - const { t } = useTranslation(); - - const { - cardConfig = DEFAULT_DATA, - } = props; - - const { title, chartData, chartHeight } = cardConfig; - - if (Array.isArray(chartData)) { - for (let i = 0, n = chartData.length; i < n; ++i) { - const item = chartData[i]; - chartData[i] = Object.assign({}, item, { category: t(item.category), type: t(item.type) }); - } - } - - return ( - - - - ); -}; - -export default CardGroupBarChart; diff --git a/frontend/src/components/CardLineChart/index.module.css b/frontend/src/components/CardLineChart/index.module.css deleted file mode 100644 index 535a04ab..00000000 --- a/frontend/src/components/CardLineChart/index.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.cardSubTitle { - color: var(--color-text1-2, #999); - font-size: 12px; - line-height: 22px; - letter-spacing: 0; -} - -.cardDes { - margin-top: 6px; - color: var(--color-text1-2, #999); - font-size: 12px; - line-height: 17px; - letter-spacing: 0; -} - -.cardDes span { - margin-left: 2px; - color: #36cfc9; - font-weight: bold; -} - -.cardValue { - margin-top: 10px; - color: #333; - font-weight: var(--font-weight-3, bold); - font-size: 28px; - line-height: 28px; - letter-spacing: 0; -} - -.cardFoot { - padding: 13px 16px; - color: #666; - font-size: 12px; -} \ No newline at end of file diff --git a/frontend/src/components/CardLineChart/index.tsx b/frontend/src/components/CardLineChart/index.tsx deleted file mode 100644 index 9ba21de2..00000000 --- a/frontend/src/components/CardLineChart/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { TinyLine } from '@ant-design/charts'; -import { Card } from 'antd'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import styles from './index.module.css'; -import mock from './mock'; - -interface CardConfig { - title?: string | React.ReactNode; - subTitle?: string | React.ReactNode; - value?: string; - values?: number[]; - nums?: number[]; - des?: string; - rate?: string; - chartHeight?: number; -} - -const DEFAULT_DATA: CardConfig = { - subTitle: 'chart.line.defaultData.subTitle', - value: mock.value, - values: mock.values, - nums: mock.nums, - des: 'chart.line.defaultData.desc:', - rate: '10.1', - chartHeight: 100, -}; - -export interface CardLineChartProps { - cardConfig?: CardConfig; -} - -const CardLineChart: React.FunctionComponent = (props: CardLineChartProps): JSX.Element => { - const { t } = useTranslation(); - - const { - cardConfig = DEFAULT_DATA, - } = props; - - const { title, subTitle, value, values, des, rate, chartHeight } = cardConfig; - - return ( - -
{typeof subTitle === 'string' ? t(subTitle) : subTitle}
-
{value}
-
{des ? t(des) : des}{rate}↑
- -
- ); -}; - -export default CardLineChart; diff --git a/frontend/src/components/CardLineChart/mock.ts b/frontend/src/components/CardLineChart/mock.ts deleted file mode 100644 index 1750191a..00000000 --- a/frontend/src/components/CardLineChart/mock.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default { - value: '123,456', - values: [3, 9, 5, 8, 11, 6, 8, 7], - nums: [1, 2, 2, 2, 2, 2, 2, 2], - dailySale: '10', -}; diff --git a/frontend/src/components/CardPieChart/index.module.css b/frontend/src/components/CardPieChart/index.module.css deleted file mode 100644 index bb5fbc32..00000000 --- a/frontend/src/components/CardPieChart/index.module.css +++ /dev/null @@ -1,8 +0,0 @@ -.radioGroup { - display: flex; - text-align: center; -} - -.radioFlex { - flex: 1; -} diff --git a/frontend/src/components/CardPieChart/index.tsx b/frontend/src/components/CardPieChart/index.tsx deleted file mode 100644 index 911c6530..00000000 --- a/frontend/src/components/CardPieChart/index.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Pie } from '@ant-design/charts'; -import type { RadioChangeEvent } from 'antd'; -import { Card, Radio } from 'antd'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import styles from './index.module.css'; - -const { useState } = React; - -interface CardConfig { - title?: string; - value?: number; - chartData?: Array>; - chartHeight?: number; -} - -const DEFAULT_DATA: CardConfig = { - title: 'chart.pie.defaultData.title', - value: 183112, - chartData: [ - { - type: 'chart.pie.defaultData.sample_1', - value: 40, - }, - { - type: 'chart.pie.defaultData.sample_2', - value: 21, - }, - { - type: 'chart.pie.defaultData.sample_3', - value: 17, - }, - { - type: 'chart.pie.defaultData.sample_4', - value: 13, - }, - { - type: 'chart.pie.defaultData.sample_5', - value: 9, - }, - ], - chartHeight: 500, -}; - -export interface CardPieChartProps { - cardConfig?: CardConfig; -} - -const CardPieChart: React.FunctionComponent = (props): JSX.Element => { - const { t } = useTranslation(); - - const { - cardConfig = DEFAULT_DATA, - } = props; - - let { title, chartData, chartHeight } = cardConfig; - - if (Array.isArray(chartData)) { - for (let i = 0, n = chartData.length; i < n; ++i) { - const item = chartData[i]; - chartData[i] = Object.assign({}, item, { - type: typeof item.type === 'string' ? t(item.type) : item.type, - title: typeof item.title === 'string' ? t(item.title) : item.title, - }); - } - } - - const [type, setType] = useState('one'); - const changeType = (e: RadioChangeEvent) => { - setType(e.target.value); - }; - - return ( - - - - {t('chart.pie.category_1')} - - - {t('chart.pie.category_2')} - - - {t('chart.pie.category_3')} - - - `${(percent * 100).toFixed(0)}%`, - }} - radius={1} - innerRadius={0.64} - meta={{ - value: { - formatter: (v) => `¥ ${v}`, - }, - }} - statistic={{ - title: { - offsetY: -8, - }, - content: { - offsetY: -4, - }, - }} - interactions={[ - { type: 'element-selected' }, - { type: 'element-active' }, - { - type: 'pie-statistic-active', - cfg: { - start: [ - { trigger: 'element:mouseenter', action: 'pie-statistic:change' }, - { trigger: 'legend-item:mouseenter', action: 'pie-statistic:change' }, - ], - end: [ - { trigger: 'element:mouseleave', action: 'pie-statistic:reset' }, - { trigger: 'legend-item:mouseleave', action: 'pie-statistic:reset' }, - ], - }, - }, - ]} - /> - - ); -}; - -export default CardPieChart; diff --git a/frontend/src/components/CardRankChart/index.module.css b/frontend/src/components/CardRankChart/index.module.css deleted file mode 100644 index 2ef1626e..00000000 --- a/frontend/src/components/CardRankChart/index.module.css +++ /dev/null @@ -1,115 +0,0 @@ -.hisMap { - width: 100%; - height: 345px; - background: url(https://img.alicdn.com/imgextra/i1/O1CN01FjHZo31W10rlpgULD_!!6000000002727-2-tps-700-348.png) 50% 50% no-repeat; - background-size: contain; -} - -.histogram { - display: flex; - flex-direction: column; - justify-content: flex-start; - width: 100%; - height: 100%; - padding: 20px; -} - -.histogram .hisTitle { - color: #999; - margin-bottom: 10px; - font-size: 12px; - line-height: 12px; -} - -.histogram .hisBody { - height: 20px; -} - -.histogram .hisRate { - margin-left: 10px; - font-weight: bold; - font-size: 14px; - line-height: 20px; -} - -.card { - height: 100%; - background-color: #fff; - border-radius: px; -} - -.card .title { - margin-top: 24px; - margin-left: 24px; - color: #333; - font-weight: bold; -} - -.card .titleDiv { - margin-bottom: 0; -} - -.subCard { - display: flex; - height: 100%; -} - -.subCard .subDiv { - height: 100%; - margin: 0; -} - -.subCard .subFooter { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin-top: 50px; -} - -.subCard .subFooter div:first-child { - color: rgba(153, 153, 153, 1); -} - -.subCard .subFooter div:nth-child(2) { - color: rgba(54, 207, 201, 1); - font-size: 32px; -} - -.subCard .subBody { - width: 100%; -} - -.subCard .subBody .subName { - color: #222; - font-weight: bold; - font-size: 16px; - line-height: 50px; - text-align: center; -} - -.subCard .subBody .subName+div { - width: 100%; - margin-top: 0; -} - -.subCard .subMain { - margin-top: 50px; - display: flex; - align-items: center; - justify-content: center; -} - -.subCard .subMain .subTypeName { - color: rgba(153, 153, 153, 1); - font-size: 12px; -} - -.subCard .subMain .subMainDiv { - height: 20px; -} - -.subCard .subMain .subTypeValue { - font-weight: bold; - font-size: 20px; -} diff --git a/frontend/src/components/CardRankChart/index.tsx b/frontend/src/components/CardRankChart/index.tsx deleted file mode 100644 index 1e1833d3..00000000 --- a/frontend/src/components/CardRankChart/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Card, Col, Divider, Row } from 'antd'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import styles from './index.module.css'; - -interface DataItem { - name?: string; - rate?: string; - color?: string; -} - -interface CardConfig { - title?: string; - dataSource?: DataItem[]; -} - -export interface CardRankChartProps { - cardConfig?: CardConfig; -} - -const DEFAULT_DATA: CardConfig = { - title: 'chart.rank.defaultData.title', - dataSource: [ - { name: 'chart.rank.defaultData.asia', rate: '40%', color: '#2B7FFB' }, - { name: 'chart.rank.defaultData.europe', rate: '30%', color: '#00D6CB' }, - { name: 'chart.rank.defaultData.africa', rate: '20%', color: '#F0C330' }, - { name: 'chart.rank.defaultData.america', rate: '10%', color: '#3840D9' }, - ], -}; - -const CardRankChart: React.FunctionComponent = (props: CardRankChartProps): JSX.Element => { - const { t } = useTranslation(); - - const { cardConfig = DEFAULT_DATA } = props; - let { title, dataSource } = cardConfig; - if (typeof title === 'string') { - title = t(title) || title; - } - if (Array.isArray(dataSource)) { - for (let i = 0, n = dataSource.length; i < n; ++i) { - const item = dataSource[i]; - dataSource[i] = Object.assign({}, item, { - name: typeof item.name === 'string' ? t(item.name) : item.name, - }); - } - } - - return ( - - - -
- - -
- {dataSource && - dataSource.map((item, idx) => ( -
-
{item.name}
-
-
-
{item.rate}
-
-
- ))} -
- - -
- -
-
{t('chart.rank.defaultData.asia')}
- -
-
-
{t('chart.rank.defaultData.category_1')}
-
6,123
-
- -
-
{t('chart.rank.defaultData.category_2')}
-
1,324
-
-
-
-
{t('chart.rank.defaultData.des')}
-
45%↑
-
-
-
- - - - - ); -}; - -export default CardRankChart; diff --git a/frontend/src/components/CardTypebarChart/index.module.css b/frontend/src/components/CardTypebarChart/index.module.css deleted file mode 100644 index 535a04ab..00000000 --- a/frontend/src/components/CardTypebarChart/index.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.cardSubTitle { - color: var(--color-text1-2, #999); - font-size: 12px; - line-height: 22px; - letter-spacing: 0; -} - -.cardDes { - margin-top: 6px; - color: var(--color-text1-2, #999); - font-size: 12px; - line-height: 17px; - letter-spacing: 0; -} - -.cardDes span { - margin-left: 2px; - color: #36cfc9; - font-weight: bold; -} - -.cardValue { - margin-top: 10px; - color: #333; - font-weight: var(--font-weight-3, bold); - font-size: 28px; - line-height: 28px; - letter-spacing: 0; -} - -.cardFoot { - padding: 13px 16px; - color: #666; - font-size: 12px; -} \ No newline at end of file diff --git a/frontend/src/components/CardTypebarChart/index.tsx b/frontend/src/components/CardTypebarChart/index.tsx deleted file mode 100644 index dcb6ae71..00000000 --- a/frontend/src/components/CardTypebarChart/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { RingProgress } from '@ant-design/charts'; -import { Card } from 'antd'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import styles from './index.module.css'; -import mock from './mock'; - -interface CardConfig { - title?: string | React.ReactNode; - subTitle?: string | React.ReactNode; - value?: string; - chartData?: number; - des?: string; - rate?: string; - chartHeight?: number; -} - -const DEFAULT_DATA: CardConfig = { - subTitle: 'chart.typebar.defaultData.subTitle', - value: mock.value, - chartData: mock.salePercent, - des: 'chart.typebar.defaultData.des', - rate: '10.1', - chartHeight: 100, -}; - -export interface CardTypebarChartProps { - cardConfig?: CardConfig; -} - -const CardTypebarChart: React.FunctionComponent = (props: CardTypebarChartProps): JSX.Element => { - const { t } = useTranslation(); - - const { - cardConfig = DEFAULT_DATA, - } = props; - - let { title, subTitle, value, des, rate, chartHeight, chartData } = cardConfig; - if (typeof title === 'string') { - title = t(title) || title; - } - if (typeof subTitle === 'string') { - subTitle = t(subTitle) || subTitle; - } - if (des) { - des = t(des) || des; - } - - return ( - -
{subTitle}
-
{value}
-
{des}{rate}↑
- -
- ); -}; - -export default CardTypebarChart; diff --git a/frontend/src/components/CardTypebarChart/mock.ts b/frontend/src/components/CardTypebarChart/mock.ts deleted file mode 100644 index dca455a5..00000000 --- a/frontend/src/components/CardTypebarChart/mock.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - value: '82,234', - salePercent: 0.7, - dailySale: '10', -}; diff --git a/frontend/src/interfaces/service-source.ts b/frontend/src/interfaces/service-source.ts index 02496493..30268992 100644 --- a/frontend/src/interfaces/service-source.ts +++ b/frontend/src/interfaces/service-source.ts @@ -16,7 +16,6 @@ export interface ServiceSourceProperties { nacosNamespaceId?: string; nacosGroups?: string[]; zkServicesPath?: string[]; - consulNamespace?: string; } /** diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json index 7583f7d1..35b021a5 100644 --- a/frontend/src/locales/en-US/translation.json +++ b/frontend/src/locales/en-US/translation.json @@ -43,7 +43,6 @@ "buttonText": "Login", "loginSuccess": "Login successful!", "loginFailed": "Login failed. Please try again.", - "incorrectCredentials": "Incorrect username or password.", "autoLogin": "Auto login", "forgotPassword": "Forgot password", "usernamePlaceholder": "Username", @@ -53,57 +52,88 @@ }, "aiRoute": { "columns": { - "name": "name", - "domains": "domains", - "upstreams": "upstreams", - "auth": "request auth", - "action": "action" + "name": "Name", + "domains": "Domains", + "upstreams": "AI Services", + "auth": "Request Auth" + }, + "routeForm": { + "label": { + "authConfig": "Request Authentication", + "authConfigExtra": "If enabled, only requests containing the auth info of a selected consumer are allowed to use this route.", + "authConfigList": "Allowed Consumers", + "domain": "Domain", + "targetModel": "Target Model", + "fallbackConfig": "Token Fallback", + "fallbackConfigExtra": "If enabled, gateway will forward the request to the fallback service if error occurs when requesting the target service.", + "fallbackUpstream": "Fallback Service", + "name": "Name", + "serviceName": "Service Name", + "serviceWeight": "Weight", + "services": "Target Services" + + }, + "rule": { + "matchTypeRequired": "Please select a match type", + "matchValueRequired": "Please input the match value", + "modelNameRequired": "Please input the model name", + "serviceWeightRequired": "Please input the weight value", + "fallbackUpstreamRequired": "Please select the fallback service", + "nameRequired": "Shall only contain upper-and lower-case letters, digits, dashes (-) and dots (.). And cannot end with a dash (-) or dot (.).", + "targetServiceRequired": "Please select a target service" + }, + "modelMatchType": "Model Match Type", + "modelMatchValue": "Match Value", + "byModelName": "By model name", + "byWeight": "By weight", + "addTargetService": "Add target AI service", + "addModelPredicate": "Add model match rule", + "selectModelService": "Select an AI service" }, - "create": "Create an AI route", - "edit": "Edit AI route", - "deleteConfirmation": "Are you sure you want to delete < 1 > {{currentConsumerName}} ?", - "authNotEnabled": "Authentication not enabled", - "authEnabledWithoutConsumer": "No one is authorized to access" + "create": "Create AI Route", + "edit": "Edit AI Route", + "deleteConfirmation": "Are you sure you want to delete <1>{{currentRouteName}}?", + "authNotEnabled": "Not enabled", + "authEnabledWithoutConsumer": "Nobody authorized", + "usage": "Usage", + "aiRouteUsage": "AI Route Usage", + "aiRouteUsageContent": "You can send a test request using the command below:" }, "consumer": { "columns": { - "name": "Consumer Name", - "credentialTypes": "Auth Methods", - "action": "Action" + "name": "Name", + "authMethods": "Auth Methods" }, "create": "Create Consumer", "edit": "Edit Consumer", - "deleteConfirmation": "Are you sure you want to delete < 1 > {{currentConsumerName}} ?", + "deleteConfirmation": "Are you sure you want to delete <1>{{currentConsumerName}}?", "consumerForm": { "name": "Name", - "namePlaceholder": "Please enter a consumer name", - "nameRequired": "Please enter a consumer name" + "nameRequired": "Please input consumer name", + "tokenSourceRequired": "Please choose a token source", + "authTokenRequired": "Please input auth token", + "headerNameRequired": "Please input header name", + "paramNameRequired": "Please input parameter key" }, "selectBEARER": "Authorization: Bearer ${value}", "selectHEADER": "Custom HTTP Header", - "selectQUERY": "Query parameter", + "selectQUERY": "Query Parameter", "tokenSource": "Token Source", "authToken": "Auth Token", - "underDevelopment": "Under development, stay tuned.", - "randomGeneration": "Randomize", - "headerName": "Header name", - "paramName": "Parameter name", - "deleteSuccess": "delete success", - "deleteFailed": "delete failed", - "createFailed": "creat failed", - "editSuccess": "edit successfully", - "editFailed": "edit failed" + "randomGeneration": "Generate", + "headerName": "Header Name", + "paramName": "Param Name", + "deleteSuccess": "Deleted successfully." }, "domain": { "columns": { "name": "Domain", "protocol": "Protocol", - "certificate": "Certificate", - "action": "Action" + "certificate": "Certificate" }, "defaultDomain": "Default Domain", "createDomain": "Create Domain", - "EditDomain": "Edit Domain", + "editDomain": "Edit Domain", "deleteConfirmation": "Are you sure you want to delete <1>{{currentDomainName}}?", "domainForm": { "name": "Domain", @@ -153,86 +183,51 @@ "columns": { "type": "Type", "name": "Name", - "tokens": "authentication token", - "action": "action" + "tokens": "Tokens" }, "providerTypes": { "openai": "OpenAI", "qwen": "Tongyi Qianwen", - "moonshot": "Dark Side of the Moon" + "moonshot": "Moonshot" }, "providerForm": { "label": { - "type": "LLM vendor", - "name": "Name", + "type": "LLM Provider", + "failoverEnabled": "Token Failover", + "failoverEnabledExtra": "If enabled, when the count of failed requests with a certain auth token exceeds the threshold, Higress will no long send requests with it, until following health check requests get a certain number of successes.", + "failureThreshold": "Min Consecuitive Failures to Mark Down a Token", + "healthCheckInterval": "Health Check Request Interval", + "healthCheckModel": "Health Check Request LLM Model", + "healthCheckTimeout": "Health Check Request Timeout", + "protocol": "Request Procotol", "serviceName": "Service Name", - "domain": "Domain name", - "protocol": "Agreement", - "modelPredicate": "Whether to enable model-based route matching rules", - "extra": "When enabled, if the request to the target service fails, the gateway will instead request a downgraded service.", - "modelPredicatePrefix": "model name matching rule", - "aiName": "Target AI Service", - "aiNameExtra": "Create AI services", - "fallbackConfig": "Downgrade service", - "fallbackConfigExtra": "When enabled, if the request to the target service fails, the gateway will instead request a downgraded service.", - "fallbackConfigList": "Downgrade service list", - "routeStrategy": "Routing downgrade strategy", - "routeStrategy1": "Randomly select a service provider in the list to downgrade, only downgrade once (default) ", - "routeStrategy2": "Attempt downgrades one by one in the order of the list of service providers until the request succeeds or all providers have tried", - "authConfig": "Whether to enable request authentication", - "authConfigExtra": "When enabled, only requests containing the specified consumer authentication information can request this route.", - "authConfigList": "List of consumer names allowed to request this route", - "weight": "weight" - }, - "placeholder": { - "type": "Please select LLM vendor", - "name": "Please enter a name", - "protocol": "Please select a protocol", - "serviceName": "Please enter a service name", - "tokens": "Please enter authentication token", - "domain": "Please enter domain name", - "aiName": "Please select and add target AI service", - "weight": "The target AI service request ratio should add up to 100.", - "modelPredicatePrefix": "Use Glob syntax to match models, for example: model- *", - "fallbackConfigList": "Please select a list of downgrade services", - "routeStrategy": "Please select a route degradation strategy", - "authConfigList": "Please select a list of consumer names that are allowed to request this route." + "successThreshold": "Min Consecuitive Sucesses to Mark Up a Token" }, "rules": { - "modelPredicatePrefix": "Please enter the model name prefix required to match this route", - "Name": "Supports uppercase and lowercase letters, numbers, and dashes, and starts with a letter or number and does not end with a dash (-). Length should not exceed 63 characters." - }, - "createDomain": "Create a domain name", - "howToUse": "How to use", - "aiRouteUsage": "How to use AI routing", - "aiRouteUsageText": "You can send a test request with the command below:" + "tokenRequired": "Please input auth token", + "typeRequired": "Please select a LLM provider", + "serviceNameRequired": "Please input service name", + "failureThresholdRequired": "Please input min consecuitive failures to mark down a token", + "healthCheckTimeoutRequired": "Please input health check request timeout", + "healthCheckIntervalRequired": "Please input health check request interval", + "healthCheckModelRequired": "Please input health check request LLM model", + "protocol": "Please select a request protocol", + "successThresholdRequired": "Please input min consecuitive sucesses to mark up a token" + } }, - "create": "Create AI service provider", - "edit": "Edit AI service provider", - "deleteConfirmation": "Are you sure you want to delete < 1 > {{currentLlmProviderName}} ?", - "deleteRoute": "Are you sure you want to delete < 1 > {{currentRouteName}} ?", - "selectModelName": "Select Model Service", - "modelProportion": "Proportionally", - "modelName": "By model name", - "addTargetAIservice": "Add target AI service", - "addTargetServer": "Add", - "operation": "Operation", - "modelMatchingType": "Model matching type", - "exactMatch": "exact match", - "prefixMatch": "Prefix Match", - "modelNames": "Model names", - "serviceName": "service name", - "requestPercentage": "Request Percentage", - "targetModel": "TargetModel", - "modelNameTips": "Model parameters in the request body" + "create": "Create AI Service Provider", + "edit": "Edit AI Service Provider", + "deleteConfirmation": "Are you sure you want to delete <1>{{currentLlmProviderName}}?" }, "plugins": { "title": "Strategy Configuration", "subTitle": { "domain": "Domain Name: ", - "route": "Route Name: " + "route": "Route Name: ", + "aiRoute": "AI Route Name: " }, "addPlugin": "Add Plugin", + "editPlugin": "Edit Plugin", "addSuccess": "Added successfully.", "updateSuccess": "Updated successfully.", "deleteConfirmation": "Confirm delete operation?", @@ -242,7 +237,6 @@ "targetDomain": "Target Domain: ", "targetRoute": "Target Route: ", "enableStatus": "Enabled", - "dataEditor": "Configuration Editor - YAML", "globalConfigWarning": "Note: Configurations above will be applied to all domains the routes. Please edit with caution." }, "builtIns": { @@ -378,7 +372,6 @@ "sniPlaceholderForDns": "Leave it empty if only one domain is set and to be used as SNI.", "zkServicesPath": "Service Registration Root Path", "zkServicesPathTooltip": "The root path of service registration are required for Zookeeper service sources. /dubbo and /services are listened by default. The former is the default root path of dubbo services, and the latter is the default root path of Sprin gCloud services", - "zkServicesPathRequired": "Please input the root path of service registration.", "zkServicesPathPlaceholder": "/dubbo and /services are listened by default. The former is the default root path of dubbo service, and the latter is the default root path of Spring Cloud services.", "nacosNamespaceId": "ID of the Nacos Namespace", "nacosNamespaceIdPlaceholder": "Leave it empty to watch the public namespace only.", @@ -387,10 +380,6 @@ "nacosGroupsRequired": "Please input Nacos service groups.", "nacosGroupsPlaceholder": "Nacos Service Groups", "naco2PortNote": "The port calculated by \"PortAbove+1000\" shall be kept accessiable as well. Otherwise, the service source won't function properly.", - "consulNamespace": "Consul Namespace", - "consulNamespaceTooltip": "Namespace info is required for Concul service sources.", - "consulNamespaceRequired": "Please input Consul namespace.", - "consulNamespacePlaceholder": "Consul Namespace", "serviceStaticAddresses": "Service Addresses", "serviceStaticAddressesRequired": "Please input service addresses.", "serviceStaticAddressesPlaceholder": "IP:Port (One address per line)", @@ -425,7 +414,7 @@ }, "matchTypes": { "PRE": "Prefix", - "EQUAL": "Equal", + "EQUAL": "Exact", "REGULAR": "Regex" }, "unsupported": "Note: Route management feature doesn't support Kubenetes with version < 1.19.0 at the moment.", @@ -465,7 +454,6 @@ "routeNameRequired": "Only following characters are allowed: lower-case letters, numbers and special characters (- .). And the name can not begin or end with a special character.", "routeNamePlaceholder": "Only following characters are allowed: lower-case letters, numbers and special characters (- .). And the name can not begin or end with a special character.", "domain": "Domain", - "domainRequired": "Please select a domain.", "domainSearchPlaceholder": "Search domain by name. If left empty, it will match any domain.", "matchType": "Match Type", "matchTypeTooltip": "The relation among different arguments is \"and\", which means the more rules are configured, the smaller the range to match is", @@ -487,70 +475,6 @@ "targetServiceNamedPlaceholder": "Search target service by name. Multiple selections are allowed." } }, - "chart": { - "area": { - "defaultData": { - "subTitle": "Request Count", - "des": "Week on Week: " - } - }, - "bar": { - "defaultData": { - "subTitle": "Total Sales", - "des": "Week on Week: " - } - }, - "groupBar": { - "defaultData": { - "title": "Consumer Data", - "category_1": "Category 1", - "category_2": "Category 2", - "category_3": "Category 3", - "category_4": "Category 4", - "category_5": "Category 5", - "shop_1": "Shop 1", - "shop_2": "Shop 2", - "shop_3": "Shop 3" - } - }, - "line": { - "defaultData": { - "subTitle": "Stop Activity Results", - "des": "Week on Week: " - } - }, - "pie": { - "defaultData": { - "title": "Sales Percentage by Category", - "sample_1": "Sample 1", - "sample_2": "Sample 2", - "sample_3": "Sample 3", - "sample_4": "Sample 4", - "sample_5": "Sample 5" - }, - "category_1": "Category 1", - "category_2": "Category 2", - "category_3": "Category 3" - }, - "rank": { - "defaultData": { - "title": "Area Sales", - "asia": "Asia", - "europe": "Europe", - "africa": "Africa", - "america": "America", - "category_1": "Category 1", - "category_2": "Category 2", - "des": "Week on Week" - } - }, - "typebar": { - "defaultData": { - "subTitle": "Merchant Sales", - "des": "Week on Week: " - } - } - }, "tlsCertificate": { "columns": { "name": "Certificate Name", @@ -612,7 +536,6 @@ } }, "misc": { - "language": "Language", "logout": "Logout", "confirm": "Confirm", "submit": "Submit", @@ -628,6 +551,8 @@ "strategy": "Strategy", "configure": "Configure", "information": "Information", + "action": "Action", + "actions": "Actions", "seconds": "Sec(s)", "tbd": "Still in development. To be released soon...", "yes": "Yes", @@ -637,4 +562,4 @@ "isRequired": "is required", "invalidSchema": "Since schema information cannot be properly parsed, this plugin only supports YAML editing." } -} \ No newline at end of file +} diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index 4565b30b..27057297 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -43,7 +43,6 @@ "buttonText": "登录", "loginSuccess": "登录成功!", "loginFailed": "登录失败,请重试!", - "incorrectCredentials": "账户或密码错误", "autoLogin": "自动登录", "forgotPassword": "忘记密码", "usernamePlaceholder": "用户名", @@ -56,27 +55,59 @@ "name": "名称", "domains": "域名", "upstreams": "服务", - "auth": "请求授权", - "action": "操作" + "auth": "请求授权" + }, + "routeForm": { + "label": { + "authConfig": "是否启用请求认证", + "authConfigExtra": "启用后,只有包含指定消费者认证信息的请求可以请求本路由。", + "authConfigList": "允许请求本路由的消费者名称列表", + "domain": "域名", + "targetModel": "目标模型", + "fallbackConfig": "降级配置", + "fallbackConfigExtra": "启用后,若请求目标服务失败,网关会改为请求降级服务。", + "fallbackUpstream": "降级服务", + "name": "名称", + "serviceName": "服务名称", + "serviceWeight": "请求比例", + "services": "目标AI服务" + }, + "rule": { + "matchTypeRequired": "请选择匹配方式", + "matchValueRequired": "请输入匹配规则", + "modelNameRequired": "请输入模型名称", + "serviceWeightRequired": "请输入请求比例", + "fallbackUpstreamRequired": "请选择降级服务", + "nameRequired": "包含小写字母、数字和以及特殊字符(- .),且不能以特殊字符开头和结尾", + "targetServiceRequired": "请选择目标服务" + }, + "modelMatchType": "匹配方式", + "modelMatchValue": "匹配条件", + "byModelName": "按模型名称", + "byWeight": "按比例", + "addTargetService": "添加目标AI服务", + "addModelPredicate": "添加模型匹配规则", + "selectModelService": "选择模型服务" }, "create": "创建AI路由", "edit": "编辑AI路由", - "deleteConfirmation": "确定删除 <1>{{currentConsumerName}} 吗?", + "deleteConfirmation": "确定删除 <1>{{currentRouteName}} 吗?", "authNotEnabled": "未开启认证", - "authEnabledWithoutConsumer": "未授权任何人访问" + "authEnabledWithoutConsumer": "未授权任何人访问", + "usage": "使用方法", + "aiRouteUsage": "AI路由使用方法", + "aiRouteUsageContent": "可使用以下命令发送请求:" }, "consumer": { "columns": { "name": "消费者名称", - "credentialTypes": "认证方式", - "action": "操作" + "authMethods": "认证方式" }, "create": "创建消费者", "edit": "编辑消费者", "deleteConfirmation": "确定删除 <1>{{currentConsumerName}} 吗?", "consumerForm": { "name": "消费者名称", - "namePlaceholder": "请输入消费者名称", "nameRequired": "请输入消费者名称", "tokenSourceRequired": "请选择令牌来源", "authTokenRequired": "请输入认证令牌", @@ -88,21 +119,16 @@ "selectQUERY": "查询参数", "tokenSource": "令牌来源", "authToken": "认证令牌", - "underDevelopment": "开发中,敬请期待", "randomGeneration": "随机生成", "headerName": "Header 名称", "paramName": "参数名称", - "deleteSuccess": "删除成功", - "deleteFailed": "删除失败", - "createFailed": "创建失败", - "editFailed": "编辑失败" + "deleteSuccess": "删除成功" }, "domain": { "columns": { "name": "域名", "protocol": "协议", - "certificate": "证书", - "action": "操作" + "certificate": "证书" }, "defaultDomain": "缺省域名", "createDomain": "创建域名", @@ -156,8 +182,7 @@ "columns": { "type": "类型", "name": "名称", - "tokens": "凭证", - "action": "操作" + "tokens": "凭证" }, "providerTypes": { "openai": "OpenAI", @@ -167,76 +192,41 @@ "providerForm": { "label": { "type": "大模型供应商", - "name": "名称", - "serviceName": "服务名称", - "domain": "域名", + "failoverEnabled": "令牌降级", + "failoverEnabledExtra": "启用后,若某一认证令牌返回异常响应的数量超出网值,Higress 将暂停使用该令牌发起请求,直至后续健康检测请求连续收到一定数量的正常响应。", + "failureThreshold": "令牌不可用时需满足的最小连续请求失败次数", + "healthCheckInterval": "健康检测请求发起间隔", + "healthCheckModel": "健康检测请求使用的模型名称", + "healthCheckTimeout": "健康检测请求超时时间", "protocol": "协议", - "modelPredicate": "是否启用基于模型的路由匹配规则", - "extra": "启用后,若请求目标服务失败,网关会改为请求降级服务。", - "modelPredicatePrefix": "模型名称匹配规则", - "aiName": "目标AI服务", - "aiNameExtra": "创建AI服务", - "fallbackConfig": "降级服务", - "fallbackConfigExtra": "启用后,若请求目标服务失败,网关会改为请求降级服务。", - "fallbackConfigList": "降级服务列表", - "routeStrategy": "路由降级策略", - "routeStrategy1": "随机选择列表中的一个服务提供商进行降级,只降级一次(默认)", - "routeStrategy2": "按服务提供商列表顺序逐个尝试降级,直至请求成功或所有提供商均尝试过为止", - "authConfig": "是否启用请求认证", - "authConfigExtra": "启用后,只有包含指定消费者认证信息的请求可以请求本路由。", - "authConfigList": "允许请求本路由的消费者名称列表", - "weight": "请求比例" - }, - "placeholder": { - "type": "请选择大模型供应商", - "name": "请输入名称", - "protocol": "请选择协议", - "serviceName": "请输入服务名称", - "tokens": "请输入凭证", - "domain": "请输入域名", - "aiName": "请选择并添加目标AI服务", - "weight": "目标AI服务请求比例相加应等于 100", - "modelPredicatePrefix": "使用 Glob 语法匹配模型,例如:model-*", - "fallbackConfigList": "请选择降级服务列表", - "routeStrategy": "请选择路由降级策略", - "authConfigList": "请选择允许请求本路由的消费者名称列表" + "serviceName": "服务名称", + "successThreshold": "令牌可用时需满足的最小连续健康检测成功次数" }, "rules": { - "modelPredicatePrefix": "请输入匹配本路由所需的模型名称前缀", - "name": "支持大小写字母、数字和短划线,并以字母或数字开头,且不以短划线(-)结尾。长度不超过63个字符。", - "tokenRequired": "请输入凭证" - }, - "createDomain": "创建域名", - "howToUse": "使用方法", - "aiRouteUsage": "AI路由使用方法", - "aiRouteUsageText": "可使用以下命令发送请求:" + "tokenRequired": "请输入凭证", + "typeRequired": "请选择大模型供应商", + "serviceNameRequired": "请输入服务名称", + "failureThresholdRequired": "请输入最小连续请求失败次数", + "healthCheckTimeoutRequired": "请输入健康检测请求超时时间", + "healthCheckIntervalRequired": "请输入健康检测请求发起间隔", + "healthCheckModelRequired": "请输入健康检测请求使用的模型名称", + "protocol": "请选择请求协议", + "successThresholdRequired": "请输入最小连续健康检测成功次数" + } }, "create": "创建AI服务提供者", "edit": "编辑AI服务提供者", - "deleteConfirmation": "确定删除 <1>{{currentLlmProviderName}} 吗?", - "deleteRoute": "确定删除 <1>{{currentRouteName}} 吗?", - "selectModelName": "选择模型服务", - "modelProportion": "按比例", - "modelName": "按模型名称", - "addTargetAIservice": "添加目标AI服务", - "addTargetServer": "添加", - "operation": "操作", - "modelMatchingType": "模型匹配方式", - "exactMatch": "精确匹配", - "prefixMatch": "前缀匹配", - "modelNames": "模型名称", - "serviceName": "服务名称", - "requestPercentage": "请求比例", - "targetModel": "目标模型", - "modelNameTips": "请求 body 中的 model 参数" + "deleteConfirmation": "确定删除 <1>{{currentLlmProviderName}} 吗?" }, "plugins": { "title": "策略配置", "subTitle": { "domain": "域名名称:", - "route": "路由名称:" + "route": "路由名称:", + "aiRoute": "AI路由名称:" }, "addPlugin": "添加插件", + "editPlugin": "编辑插件", "addSuccess": "添加成功", "updateSuccess": "修改成功", "deleteConfirmation": "是否确认删除?", @@ -246,7 +236,6 @@ "targetDomain": "作用域名:", "targetRoute": "作用路由:", "enableStatus": "开启状态", - "dataEditor": "数据编辑器 - YAML", "globalConfigWarning": "注意:以上配置将会在所有域名和路由上生效。请谨慎配置。" }, "builtIns": { @@ -382,7 +371,6 @@ "sniPlaceholderForDns": "若服务来源只关联了一个域名且使用与该域名相同的SNI,此处可留空。", "zkServicesPath": "服务注册根路径", "zkServicesPathTooltip": "Zookeeper类型的服务来源需要填写服务注册的根路径。默认监听/dubbo和/services。前者为dubbo服务默认根路径,后者为Spring Cloud服务默认根路径。", - "zkServicesPathRequired": "请输入服务注册根路径", "zkServicesPathPlaceholder": "默认监听/dubbo和/services。前者为dubbo服务默认根路径,后者为Spring Cloud服务默认根路径。", "nacosNamespaceId": "Nacos命名空间ID", "nacosNamespaceIdPlaceholder": "留空表示仅监听public命名空间", @@ -391,10 +379,6 @@ "nacosGroupsRequired": "请输入Nacos服务分组列表", "nacosGroupsPlaceholder": "Nacos服务分组列表", "naco2PortNote": "“以上端口+1000”所得到的端口应同时保持畅通,否则本服务来源将无法正常工作。", - "consulNamespace": "Consul命名空间", - "consulNamespaceTooltip": "Consul类型的服务来源需要填写命名空间", - "consulNamespaceRequired": "请输入Concul命名空间", - "consulNamespacePlaceholder": "Consul命名空间", "serviceStaticAddresses": "服务地址", "serviceStaticAddressesRequired": "请输入服务地址", "serviceStaticAddressesPlaceholder": "IP:Port(多个地址请以换行分隔)", @@ -469,7 +453,6 @@ "routeNameRequired": "包含小写字母、数字和以及特殊字符(- .),且不能以特殊字符开头和结尾", "routeNamePlaceholder": "包含小写字母、数字和以及特殊字符(- .),且不能以特殊字符开头和结尾", "domain": "域名", - "domainRequired": "请选择域名", "domainSearchPlaceholder": "根据域名名称搜索域名。若留空,则表示路由可匹配任意域名", "matchType": "匹配规则", "matchTypeTooltip": "规则之间是“与”关系,即填写的规则越多,匹配的范围越小", @@ -491,70 +474,6 @@ "targetServiceNamedPlaceholder": "搜索服务名称选择服务,可多选" } }, - "chart": { - "area": { - "defaultData": { - "subTitle": "访问量", - "des": "周同比:" - } - }, - "bar": { - "defaultData": { - "subTitle": "总销售额", - "des": "周同比:" - } - }, - "groupBar": { - "defaultData": { - "title": "消费数据", - "category_1": "品类一", - "category_2": "品类二", - "category_3": "品类三", - "category_4": "品类四", - "category_5": "品类五", - "shop_1": "门店一", - "shop_2": "门店二", - "shop_3": "门店三" - } - }, - "line": { - "defaultData": { - "subTitle": "门店活动效果", - "des": "周同比:" - } - }, - "pie": { - "defaultData": { - "title": "销售额类别占比", - "sample_1": "事例一", - "sample_2": "事例二", - "sample_3": "事例三", - "sample_4": "事例四", - "sample_5": "事例五" - }, - "category_1": "类目一", - "category_2": "类目二", - "category_3": "类目三" - }, - "rank": { - "defaultData": { - "title": "区域销售", - "asia": "亚洲", - "europe": "欧洲", - "africa": "非洲", - "america": "美洲", - "category_1": "商品类目1", - "category_2": "商品类目2", - "des": "周同比" - } - }, - "typebar": { - "defaultData": { - "subTitle": "商品销售", - "des": "周同比:" - } - } - }, "tlsCertificate": { "columns": { "name": "证书名称", @@ -616,7 +535,6 @@ } }, "misc": { - "language": "语言", "logout": "退出登录", "confirm": "确定", "submit": "提交", @@ -632,8 +550,10 @@ "strategy": "策略", "configure": "配置", "information": "信息", + "action": "操作", + "actions": "操作", "seconds": "秒", - "tbd": "页面开发中,即将推出...", + "tbd": "功能开发中,即将推出...", "yes": "是", "no": "否", "switchToYAML": "YAML视图", diff --git a/frontend/src/pages/ai/components/ProviderForm/index.tsx b/frontend/src/pages/ai/components/ProviderForm/index.tsx index 87f7e609..3f1f4b79 100644 --- a/frontend/src/pages/ai/components/ProviderForm/index.tsx +++ b/frontend/src/pages/ai/components/ProviderForm/index.tsx @@ -36,7 +36,7 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { healthCheckModel, } = tokenFailoverConfig ?? {}; - const localFailoverEnabled = tokenFailoverConfig?.enabled || false; + const localFailoverEnabled = !!tokenFailoverConfig?.enabled; setFailoverEnabled(localFailoverEnabled); form.setFieldsValue({ name, @@ -44,10 +44,10 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { protocol, tokens, failoverEnabled: localFailoverEnabled, - failureThreshold, - successThreshold, - healthCheckInterval, - healthCheckTimeout, + failureThreshold: failureThreshold || 1, + successThreshold: successThreshold || 1, + healthCheckInterval: healthCheckInterval || 5000, + healthCheckTimeout: healthCheckTimeout || 10000, healthCheckModel, }) } @@ -68,20 +68,17 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { type: values.type, name: values.name, tokens: values.tokens, - version: 0, // 资源版本号。进行创建或强制更新操作时需设置为 0 + version: 0, protocol: values.protocol, tokenFailoverConfig: { enabled: values.failoverEnabled, + failureThreshold: values.failureThreshold, + successThreshold: values.successThreshold, + healthCheckInterval: values.healthCheckInterval, + healthCheckTimeout: values.healthCheckTimeout, + healthCheckModel: values.healthCheckModel, }, - } - - if (values.failoverEnabled) { - result.tokenFailoverConfig['failureThreshold'] = values.failureThreshold; - result.tokenFailoverConfig['successThreshold'] = values.successThreshold; - result.tokenFailoverConfig['healthCheckInterval'] = values.healthCheckInterval; - result.tokenFailoverConfig['healthCheckTimeout'] = values.healthCheckTimeout; - result.tokenFailoverConfig['healthCheckModel'] = values.healthCheckModel; - } + }; return result; }, @@ -100,13 +97,12 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { rules={[ { required: true, - message: t('llmProvider.providerForm.placeholder.type'), + message: t('llmProvider.providerForm.rules.typeRequired'), }, ]} > diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index 74189d66..4d74ee85 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -1,16 +1,16 @@ -import { Form, Input, Select, Switch, Button, Space, InputNumber, AutoComplete } from 'antd'; -import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons'; +import { Consumer } from '@/interfaces/consumer'; +import { DEFAULT_DOMAIN, Domain } from '@/interfaces/domain'; import { LlmProvider } from '@/interfaces/llm-provider'; -import { getLlmProviders } from '@/services/llm-provider'; -import { Domain } from '@/interfaces/domain'; +import { getGatewayDomains } from '@/services'; import { getConsumers } from '@/services/consumer'; -import { Consumer } from '@/interfaces/consumer'; +import { getLlmProviders } from '@/services/llm-provider'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { useRequest } from 'ahooks'; -import { getGatewayDomains } from '@/services'; -import { RedoOutlinedBtn, HistoryButton } from './Components'; -import { aiModelproviders } from './const'; +import { AutoComplete, Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd'; +import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { aiModelProviders } from '../../configs'; +import { HistoryButton, RedoOutlinedBtn } from './Components'; const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const { t } = useTranslation(); @@ -19,10 +19,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const [fallbackConfig_enabled, setFallbackConfigEnabled] = useState(false); const [authConfig_enabled, setAuthConfigEnabled] = useState(false); const [upstreamsError, setUpstreamsError] = useState(false); // 目标AI服务错误提示 - const [modelService, setModelService] = useState('Proportion'); - const [modelName, setModelName] = useState(''); // 勿删 用于更新UI - const [providerIndex, setProviderIndex] = useState(''); // 勿删 用于更新UI - const [dataSource, setDataSource] = useState([]); // 勿删 用于更新UI + const [modelService, setModelService] = useState('ByWeight'); const [llmList, setLlmList] = useState([]); const llmResult = useRequest(getLlmProviders, { manual: true, @@ -42,12 +39,12 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { setConsumerList(consumers); }, }); - const [domainsList, setDomainsList] = useState([]); + const [domainList, setDomainList] = useState([]); const domainsResult = useRequest(getGatewayDomains, { manual: true, onSuccess: (result) => { - const consumers = (result || []) as Domain[]; - setDomainsList(consumers); + const domains = (result || []) as Domain[]; + setDomainList(domains.filter(d => d.name !== DEFAULT_DOMAIN)); }, }); @@ -95,7 +92,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { provider: upstreams[0].provider, })); } else { - setModelService("Proportion"); + setModelService("ByWeight"); initValues["upstreams"] = upstreams.map((item) => { let obj = { provider: item.provider, @@ -119,7 +116,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const values = await form.validateFields(); const { upstreams = [] } = values; - if (modelService === "Proportion") { + if (modelService === "ByWeight") { if (!upstreams?.length) { setUpstreamsError("aiName"); return false; @@ -145,7 +142,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { modelPredicates = [], } = values; - const isProportion = modelService === "Proportion"; + const byWeight = modelService === "ByWeight"; const payload = { name, domains: domains && !Array.isArray(domains) ? [domains] : domains, @@ -156,7 +153,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { enabled: authConfig_enabled, }, } - if (isProportion) { + if (byWeight) { payload["upstreams"] = upstreams.map(({ provider, weight, modelMapping }) => { const obj = { provider, weight, modelMapping: {} }; if (modelMapping) { @@ -187,7 +184,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const getOptionsForAi = (providerName) => { try { // 通过 【服务名称】 来筛查满足 【目标模型 预定义】 的下拉选项 - const _list = aiModelproviders.filter(item => item.value.toUpperCase().indexOf(providerName.toUpperCase()) !== -1); + const _list = aiModelProviders.filter(item => item.value.toUpperCase().indexOf(providerName.toUpperCase()) !== -1); if (_list.length) { const _filterList = _list.map(item => item.targetModelList); return _filterList.flatMap(item => item) @@ -209,17 +206,22 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { return []; }; + const canUseAsFallback = (provider): boolean => { + // TODO + return true; + }; + return (
@@ -228,25 +230,25 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { allowClear maxLength={63} disabled={value} - placeholder={t('llmProvider.providerForm.rules.name')} + placeholder={t('aiRoute.routeForm.rule.nameRequired')} />
)} + extra={()} > - + {domainList.map((item) => ({item.name}))}
@@ -275,9 +277,9 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { Key - {t("llmProvider.modelMatchingType")}{/* 模型匹配方式 */} - {t("llmProvider.modelNames")}{/* 模型名称 */} - {fields.length > 1 ? {t("llmProvider.operation")} : null}{/* 操作 */} + {t("aiRoute.routeForm.modelMatchType")}{/* 模型匹配方式 */} + {t("aiRoute.routeForm.modelMatchValue")}{/* 模型名称 */} + {fields.length > 1 ? {t("misc.action")} : null}{/* 操作 */} @@ -290,12 +292,12 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { @@ -306,7 +308,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { {...restField} name={[name, 'matchValue']} noStyle - rules={[{ required: true, message: t("llmProvider.matchValueRequired") }]} + rules={[{ required: true, message: t("aiRoute.routeForm.rule.matchValueRequired") }]} > @@ -325,7 +327,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => {
- +
) } @@ -336,12 +338,12 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { <> {fields.map(({ key, name, ...restField }, index) => ( - {llmList.map((item) => ({item.name}))} @@ -353,11 +355,11 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { <> )} + help={upstreamsError ? t(`aiRoute.routeForm.placeholder.${upstreamsError}`) : null} + extra={()} > {(fields, { add, remove }) => { @@ -366,9 +368,9 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { return ( <> -
*{t("llmProvider.serviceName")}
{/* 服务名称 */} -
*{t("llmProvider.requestPercentage")}
{/* 请求比例 */} -
{t("llmProvider.targetModel")}
{/* 目标模型 */} +
*{t("aiRoute.routeForm.label.serviceName")}
+
*{t("aiRoute.routeForm.label.serviceWeight")}
+
{t("aiRoute.routeForm.label.targetModel")}
{fields.map(({ key, name, ...restField }, index) => ( @@ -377,12 +379,10 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { {...restField} name={[name, 'provider']} style={{ marginBottom: '0.5rem' }} - rules={[{ required: true, message: t('llmProvider.providerForm.placeholder.aiName') }]} + rules={[{ required: true, message: t('aiRoute.routeForm.rule.targetServiceRequired') }]} >{/* 服务名称 */} setProviderIndex(text)}> - {llmList.map((item) => ( {item.name} ))} +
{/* 模型名称 */} setModelName(text)} filterOption={(inputValue, option: any) => option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1} allowClear /> @@ -498,10 +493,10 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { { @@ -516,11 +511,11 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { style={{ flex: 1, marginRight: '8px' }} required name="authConfig_allowedConsumers" - label={t('llmProvider.providerForm.label.authConfigList')} - rules={[{ required: true, message: t('llmProvider.providerForm.label.authConfigList') }]} + label={t('aiRoute.routeForm.label.authConfigList')} + rules={[{ required: true, message: t('aiRoute.routeForm.label.authConfigList') }]} extra={()} > - {consumerList.map((item) => ({item.name}))} diff --git a/frontend/src/pages/ai/config/index.tsx b/frontend/src/pages/ai/config/index.tsx deleted file mode 100644 index 8b179840..00000000 --- a/frontend/src/pages/ai/config/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { PageContainer } from '@ant-design/pro-layout'; -import { Button, Col, PageHeader, Row, Spin } from 'antd'; -import { RedoOutlined } from '@ant-design/icons'; -import { history, useSearchParams } from 'ice'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -export default function RouterConfig() { - const { t } = useTranslation(); - const [searchParams] = useSearchParams(); - const name = searchParams.get('name'); - - const handleBack = () => { - history?.push('/ai/route'); - }; - - const pageHeader = useMemo(() => { - return { title: '策略配置', subTitle: `AI 路由名称 ${name}` }; - }, [name]); - - return ( -
- {!!pageHeader.title && ( - - )} - - -
- - - - - -
-
-
-
- ); -} diff --git a/frontend/src/pages/ai/components/RouteForm/const.tsx b/frontend/src/pages/ai/configs.tsx similarity index 99% rename from frontend/src/pages/ai/components/RouteForm/const.tsx rename to frontend/src/pages/ai/configs.tsx index 267efaec..9495768b 100644 --- a/frontend/src/pages/ai/components/RouteForm/const.tsx +++ b/frontend/src/pages/ai/configs.tsx @@ -1,4 +1,4 @@ -export const aiModelproviders = [ +export const aiModelProviders = [ { label: 'OpenAI', value: 'openai', @@ -28,7 +28,7 @@ export const aiModelproviders = [ ], }, { - label: "Awen", + label: "Qwen", value: 'qwen', serviceAddress: 'https://dashscope.aliyuncs.com/compatible-mode/v1', modelNamePattern: 'qwen-*', diff --git a/frontend/src/pages/ai/provider.tsx b/frontend/src/pages/ai/provider.tsx index d7f49857..18fdb6bd 100644 --- a/frontend/src/pages/ai/provider.tsx +++ b/frontend/src/pages/ai/provider.tsx @@ -25,13 +25,18 @@ const EllipsisMiddle: React.FC = (params: { token: String }) => { const [isHidden, setIsHidden] = useState(true); const toggledText = () => { - if (isHidden) { - let frontKeyword = token.slice(0, 3); - let backKeyword = token.slice(-3); - return `${frontKeyword}******${backKeyword}`; - } else { + if (!isHidden) { return token; } + const prefixLength = 3; + const suffixLength = 3; + if (token.length - prefixLength - suffixLength > 6) { + return `${token.slice(0, 3)}******${token.slice(-3)}`; + } + if (token.length > 2) { + return `${token.slice(0, 1)}******${token.slice(-1)}`; + } + return `${token.slice(0, 1)}******`; }; return ( @@ -88,7 +93,7 @@ const LlmProviderList: React.FC = () => { }, }, { - title: t('llmProvider.columns.action'), + title: t('misc.actions'), dataIndex: 'action', key: 'action', width: 140, diff --git a/frontend/src/pages/ai/route.tsx b/frontend/src/pages/ai/route.tsx index 297fb7dd..7e8e56e7 100644 --- a/frontend/src/pages/ai/route.tsx +++ b/frontend/src/pages/ai/route.tsx @@ -3,12 +3,12 @@ import { addAiRoute, deleteAiRoute, getAiRoutes, updateAiRoute } from '@/service import { ArrowRightOutlined, ExclamationCircleOutlined, RedoOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-layout'; import { useRequest } from 'ahooks'; -import { Button, Col, Drawer, Form, Modal, Row, Space, Table, Typography } from 'antd'; +import { Button, Col, Drawer, Form, Modal, Row, Space, Table } from 'antd'; +import { history } from 'ice'; import React, { useEffect, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import RouteForm from './components/RouteForm'; -import { HistoryButton } from './components/RouteForm/Components'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import RouteForm from './components/RouteForm'; interface FormRef { reset: () => void; @@ -78,15 +78,15 @@ const AiRouteList: React.FC = () => { }, }, { - title: t('aiRoute.columns.action'), + title: t('misc.actions'), dataIndex: 'action', key: 'action', width: 240, align: 'center', render: (_, record) => ( - onUsageDrawer(record)}>{t('llmProvider.providerForm.howToUse')} - + onUsageDrawer(record)}>{t('aiRoute.usage')} + onEditConfig(record)}>{t('misc.strategy')} onEditDrawer(record)}>{t('misc.edit')} onShowModal(record)}>{t('misc.delete')} @@ -152,6 +152,10 @@ const AiRouteList: React.FC = () => { setUsageDrawer(false); } + const onEditConfig = (aiRoute: AiRoute) => { + history?.push(`/ai/route/config?type=aiRoute&name=${aiRoute.name}`); + }; + const onEditDrawer = (aiRoute: AiRoute) => { setCurrentAiRoute(aiRoute); setOpenDrawer(true); @@ -273,7 +277,7 @@ const AiRouteList: React.FC = () => { { , ]} > - {t("llmProvider.providerForm.aiRouteUsageText")} + {t("aiRoute.aiRouteUsageContent")} {usageCommand} @@ -298,7 +302,7 @@ const AiRouteList: React.FC = () => { okText={t('misc.confirm')} >

- + 确定删除 {{ currentRouteName: (currentAiRoute && currentAiRoute.name) || '' }} 吗?

diff --git a/frontend/src/pages/ai/route/config.tsx b/frontend/src/pages/ai/route/config.tsx new file mode 100644 index 00000000..dc7e4eb4 --- /dev/null +++ b/frontend/src/pages/ai/route/config.tsx @@ -0,0 +1,5 @@ +import PluginList from '@/pages/plugin'; + +export default function RouterConfig() { + return ; +} diff --git a/frontend/src/pages/consumer/components/ConsumerForm/index.tsx b/frontend/src/pages/consumer/components/ConsumerForm/index.tsx index a3010033..7991fb8e 100644 --- a/frontend/src/pages/consumer/components/ConsumerForm/index.tsx +++ b/frontend/src/pages/consumer/components/ConsumerForm/index.tsx @@ -75,7 +75,6 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { rules={[ { required: true, - pattern: /^(?!-)[A-Za-z0-9-]{0,62}[A-Za-z0-9]$/, message: t('consumer.consumerForm.nameRequired'), }, ]} @@ -85,10 +84,9 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { allowClear maxLength={63} disabled={value} - placeholder={t('consumer.consumerForm.namePlaceholder')} />
-
{t("consumer.columns.credentialTypes")}
+
{t("consumer.columns.authMethods")}
setActiveTabKey(key)} @@ -105,14 +103,14 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { label: 'OAuth2', key: 'oauth2', children: ( - <>{t("consumer.underDevelopment")} + <>{t("misc.tbd")} ), }, { label: 'JWT', key: 'jwt-auth', children: ( - <>{t("consumer.underDevelopment")} + <>{t("misc.tbd")} ), }, ]} diff --git a/frontend/src/pages/consumer/index.tsx b/frontend/src/pages/consumer/index.tsx index c4ba02fa..25e35b4b 100644 --- a/frontend/src/pages/consumer/index.tsx +++ b/frontend/src/pages/consumer/index.tsx @@ -40,7 +40,7 @@ const ConsumerList: React.FC = () => { ellipsis: true, }, { - title: t('consumer.columns.credentialTypes'), + title: t('consumer.columns.authMethods'), dataIndex: 'credentials', key: 'credentials', render: (value) => { @@ -70,7 +70,7 @@ const ConsumerList: React.FC = () => { }, }, { - title: t('consumer.columns.action'), + title: t('misc.actions'), dataIndex: 'action', key: 'action', width: 140, diff --git a/frontend/src/pages/domain/index.tsx b/frontend/src/pages/domain/index.tsx index 2a339295..0affc6a1 100644 --- a/frontend/src/pages/domain/index.tsx +++ b/frontend/src/pages/domain/index.tsx @@ -43,7 +43,7 @@ const DomainList: React.FC = () => { render: (value) => value || '-', }, { - title: t('domain.columns.action'), + title: t('misc.actions'), dataIndex: 'action', key: 'action', width: 200, diff --git a/frontend/src/pages/layout.tsx b/frontend/src/pages/layout.tsx index 7fe577ae..c0608b50 100644 --- a/frontend/src/pages/layout.tsx +++ b/frontend/src/pages/layout.tsx @@ -36,7 +36,7 @@ export default function Layout() { className={styles.layout} logo={logo} pure={route && !!route.usePureLayout} - title="" + title={t('index.title') || ''} location={{ pathname: location.pathname, }} diff --git a/frontend/src/pages/plugin/components/HeaderModify/index.tsx b/frontend/src/pages/plugin/components/HeaderModify/index.tsx index 6e1ef3b6..46f9a285 100644 --- a/frontend/src/pages/plugin/components/HeaderModify/index.tsx +++ b/frontend/src/pages/plugin/components/HeaderModify/index.tsx @@ -98,7 +98,7 @@ const HeaderModify = forwardRef((props, ref) => { shouldUpdate rules={[{ required: true, - message: t('plugins.builtIns.headerControl.valueRequired') || '', + message: t('plugins.builtIns.headerControl.keyRequired') || '', }]} name={[field.name, 'key']} fieldKey={[field.fieldKey, 'key']} diff --git a/frontend/src/pages/plugin/components/PluginDrawer/GlobalPluginDetail.tsx b/frontend/src/pages/plugin/components/PluginDrawer/GlobalPluginDetail.tsx index 4e6fa2cc..be967295 100644 --- a/frontend/src/pages/plugin/components/PluginDrawer/GlobalPluginDetail.tsx +++ b/frontend/src/pages/plugin/components/PluginDrawer/GlobalPluginDetail.tsx @@ -1,18 +1,23 @@ import CodeEditor from '@/components/CodeEditor'; -import { Alert, Form, Spin, Switch, message, Space, Typography, Input, Select, Divider, Tabs, Card } from 'antd'; +import { Alert, Card, Form, Input, message, Select, Space, Spin, Switch, Tabs, Typography } from 'antd'; import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import * as servicesApi from '@/services'; import { useRequest } from 'ahooks'; import i18next, { t } from 'i18next'; import { useSearchParams } from 'ice'; - -import ArrayForm from './ArrayForm'; import yaml from 'js-yaml'; +import { QueryType } from '../../utils'; +import ArrayForm from './ArrayForm'; const { Text } = Typography; const { TabPane } = Tabs; +const QUERY_TYPE_2_MESSAGE_KEY = {}; +QUERY_TYPE_2_MESSAGE_KEY[QueryType.DOMAIN] = 'plugins.configForm.targetDomain'; +QUERY_TYPE_2_MESSAGE_KEY[QueryType.ROUTE] = 'plugins.configForm.targetRoute'; +QUERY_TYPE_2_MESSAGE_KEY[QueryType.AI_ROUTE] = 'plugins.configForm.targetAiRoute'; + export interface IPluginData { configurations: object; enabled: boolean; @@ -43,33 +48,46 @@ const GlobalPluginDetail = forwardRef((props: IProps, ref) => { const queryName: string = searchParams.get('name') || ''; const [currentTabKey, setCurrentTabKey] = useState('form'); - const isChangeExampleRaw = useMemo(() => { - return ['route', 'domain'].includes(queryType) && category === 'auth'; - }, [queryType, category]); - - const isRoutePlugin = useMemo(() => { - return queryType === 'route'; + const isGlobalPlugin = useMemo(() => { + return !queryType; }, [queryType]); - const isDomainPlugin = useMemo(() => { - return queryType === 'domain'; - }, [queryType]); + const isChangeExampleRaw = useMemo(() => { + return isGlobalPlugin && category === 'auth'; + }, [isGlobalPlugin, category]); const pluginInstancesApi = useMemo(() => { - if (queryType === 'route') { + if (queryType === QueryType.ROUTE) { return { get: servicesApi.getRoutePluginInstance.bind(servicesApi), update: servicesApi.updateRoutePluginInstance.bind(servicesApi), }; } - if (queryType === 'domain') { + if (queryType === QueryType.DOMAIN) { return { get: servicesApi.getDomainPluginInstance.bind(servicesApi), update: servicesApi.updateDomainPluginInstance.bind(servicesApi), }; } + if (queryType === QueryType.AI_ROUTE) { + return { + get: (params: { name: string; pluginName: string }) => { + return servicesApi.getRoutePluginInstance({ + name: `ai-route-${params.name}.internal`, + pluginName: params.pluginName, + }); + }, + update: (params: { name: string; pluginName: string }, payload) => { + return servicesApi.updateRoutePluginInstance({ + name: `ai-route-${params.name}.internal`, + pluginName: params.pluginName, + }, payload); + }, + }; + } + return { get: servicesApi.getGlobalPluginInstance.bind(servicesApi), update: servicesApi.updateGlobalPluginInstance.bind(servicesApi), @@ -454,30 +472,12 @@ const GlobalPluginDetail = forwardRef((props: IProps, ref) => { delete params.configurations; - if (isRoutePlugin || isDomainPlugin) { - updateData( - { - name: queryName, - pluginName, - }, - params, - ); - return; - } - - updateData(pluginName, params); + updateData(isGlobalPlugin ? pluginName : { name: queryName, pluginName }, params); }; useEffect(() => { resetForm(); - if (isRoutePlugin || isDomainPlugin) { - getData({ - name: queryName, - pluginName, - }); - return; - } - getData(pluginName); + getData(isGlobalPlugin ? pluginName : { name: queryName, pluginName }); }, [pluginName, queryName]); const resetForm = () => { @@ -518,10 +518,10 @@ const GlobalPluginDetail = forwardRef((props: IProps, ref) => { const alertStatus = useMemo(() => { return { - isShow: (isRoutePlugin || isDomainPlugin) && queryName, - message: isRoutePlugin ? t('plugins.configForm.targetRoute') + queryName : t('plugins.configForm.targetDomain') + queryName, + isShow: !!isGlobalPlugin && queryName, + message: t(QUERY_TYPE_2_MESSAGE_KEY[queryType]), }; - }, [isRoutePlugin, isDomainPlugin, queryName]); + }, [queryType, queryName]); const fieldChange = () => { if (!getConfigLoading && !getDataLoading && currentTabKey === 'form') { @@ -572,7 +572,7 @@ const GlobalPluginDetail = forwardRef((props: IProps, ref) => { )} - {!getConfigLoading && !getDataLoading && !isRoutePlugin && !isDomainPlugin && ( + {!getConfigLoading && !getDataLoading && isGlobalPlugin && ( {t('plugins.configForm.globalConfigWarning')} diff --git a/frontend/src/pages/plugin/components/Retries/index.tsx b/frontend/src/pages/plugin/components/Retries/index.tsx index 6a75a61a..97517a8c 100644 --- a/frontend/src/pages/plugin/components/Retries/index.tsx +++ b/frontend/src/pages/plugin/components/Retries/index.tsx @@ -56,7 +56,7 @@ const Retries = forwardRef((props, ref) => { @@ -65,7 +65,7 @@ const Retries = forwardRef((props, ref) => { @@ -78,7 +78,7 @@ const Retries = forwardRef((props, ref) => { diff --git a/frontend/src/pages/plugin/index.tsx b/frontend/src/pages/plugin/index.tsx index a8286bf4..cf211cb9 100644 --- a/frontend/src/pages/plugin/index.tsx +++ b/frontend/src/pages/plugin/index.tsx @@ -1,5 +1,5 @@ import { WasmPluginData } from '@/interfaces/route'; -import { createWasmPlugin, deleteWasmPlugin, getGatewayRoutesDetail, updateWasmPlugin } from '@/services'; +import { createWasmPlugin, deleteWasmPlugin, getGatewayRouteDetail, updateWasmPlugin } from '@/services'; import { RedoOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-layout'; import { useRequest } from 'ahooks'; @@ -11,11 +11,17 @@ import PluginDrawer from './components/PluginDrawer'; import PluginList, { ListRef } from './components/PluginList'; import { WasmFormRef, WasmPluginDrawer } from './components/Wasm'; import styles from './index.module.css'; +import { QueryType } from './utils'; + +const QUERY_TYPE_2_BACK_PATH = {}; +QUERY_TYPE_2_BACK_PATH[QueryType.ROUTE] = '/route'; +QUERY_TYPE_2_BACK_PATH[QueryType.DOMAIN] = '/domain'; +QUERY_TYPE_2_BACK_PATH[QueryType.AI_ROUTE] = '/ai/route'; export default function RouterConfig() { const { t } = useTranslation(); - const [routerDetail, setRouterDetail] = useState({}); + const [routeDetail, setRouteDetail] = useState({}); const [searchParams] = useSearchParams(); const wasmFormRef = useRef(); @@ -31,20 +37,21 @@ export default function RouterConfig() { }; const handleBack = () => { - if (type === 'route') history?.push('/route'); - if (type === 'domain') history?.push('/domain'); + const path = QUERY_TYPE_2_BACK_PATH[type]; + path && history?.push(path); }; const pageHeader = useMemo(() => { - if (type === 'domain') return { title: t('plugins.title'), subTitle: `${t('plugins.subTitle.domain')}${name}` }; - if (type === 'route') return { title: t('plugins.title'), subTitle: `${t('plugins.subTitle.route')}${name}` }; + if (type) { + return { title: t('plugins.title'), subTitle: `${t(`plugins.subTitle.${type}`)}${name}` }; + } return { title: '', subTitle: '' }; }, [type, name]); - const { loading, run, refresh } = useRequest(getGatewayRoutesDetail, { + const { loading, run: loadRouteDetail } = useRequest(getGatewayRouteDetail, { manual: true, onSuccess: (res) => { - setRouterDetail(res || {}); + setRouteDetail(res || {}); }, }); @@ -80,7 +87,7 @@ export default function RouterConfig() { const init = () => { if (name && type === 'route') { - run(name); + loadRouteDetail(name); } }; @@ -133,12 +140,12 @@ export default function RouterConfig() {
- + diff --git a/frontend/src/pages/plugin/utils.tsx b/frontend/src/pages/plugin/utils.tsx index d4436965..743fdbab 100644 --- a/frontend/src/pages/plugin/utils.tsx +++ b/frontend/src/pages/plugin/utils.tsx @@ -1,5 +1,11 @@ import i18n from "@/i18n"; +export enum QueryType { + ROUTE = 'route', + DOMAIN = 'domain', + AI_ROUTE = 'aiRoute', +} + export function getI18nValue(obj: object, key: string) { if (!obj) { return null; diff --git a/frontend/src/services/route.ts b/frontend/src/services/route.ts index aa678fae..83064121 100644 --- a/frontend/src/services/route.ts +++ b/frontend/src/services/route.ts @@ -6,7 +6,7 @@ export const getGatewayRoutes = (): Promise => { }; // 获取指定路由 -export const getGatewayRoutesDetail = (routeName): Promise => { +export const getGatewayRouteDetail = (routeName): Promise => { return request.get(`/v1/routes/${routeName}`); };