diff --git a/web/src/components/Plugin/PluginDetail.tsx b/web/src/components/Plugin/PluginDetail.tsx index 932dee6645..2ff336c730 100644 --- a/web/src/components/Plugin/PluginDetail.tsx +++ b/web/src/components/Plugin/PluginDetail.tsx @@ -67,8 +67,8 @@ const PluginDetail: React.FC = ({ visible, readonly = false, initialData = {}, - onClose = () => { }, - onChange = () => { }, + onClose = () => {}, + onChange = () => {}, }) => { const { formatMessage } = useIntl(); const [form] = Form.useForm(); diff --git a/web/src/components/Plugin/PluginPage.tsx b/web/src/components/Plugin/PluginPage.tsx index 272b00c29e..cde28976ac 100644 --- a/web/src/components/Plugin/PluginPage.tsx +++ b/web/src/components/Plugin/PluginPage.tsx @@ -48,7 +48,7 @@ const PluginPage: React.FC = ({ initialData = {}, schemaType = 'route', type = 'scoped', - onChange = () => { }, + onChange = () => {}, }) => { const [pluginList, setPluginList] = useState([]); const [name, setName] = useState(NEVER_EXIST_PLUGIN_FLAG); diff --git a/web/src/constants.ts b/web/src/constants.ts index 217cf40504..3df1c0cf23 100644 --- a/web/src/constants.ts +++ b/web/src/constants.ts @@ -32,4 +32,4 @@ export const codeMessage = { 504: '网关超时。', }; -export const DEFAULT_GLOBAL_RULE_ID = "1" +export const DEFAULT_GLOBAL_RULE_ID = '1'; diff --git a/web/src/hooks/useForceIntl.ts b/web/src/hooks/useForceIntl.ts index 8f67bef9c1..890a16a5f0 100644 --- a/web/src/hooks/useForceIntl.ts +++ b/web/src/hooks/useForceIntl.ts @@ -29,7 +29,7 @@ const useForceIntl = () => { } const { locale } = getIntl(); - if (locale === 'zh-cn') { + if (locale === 'zh-CN') { return; } diff --git a/web/src/locales/en-US/component.ts b/web/src/locales/en-US/component.ts index 946a735ac5..fa58634786 100644 --- a/web/src/locales/en-US/component.ts +++ b/web/src/locales/en-US/component.ts @@ -28,6 +28,7 @@ export default { 'component.global.add': 'Add', 'component.global.save': 'Save', 'component.global.edit': 'Edit', + 'component.global.manage': 'Manage', 'component.global.update': 'Update', 'component.global.get': 'Get', 'component.global.edit.plugin': 'Edit plugin', @@ -35,6 +36,7 @@ export default { 'component.global.list': 'List', 'component.global.description': 'Description', 'component.global.labels': 'Labels', + 'component.global.version': 'Version', 'component.global.operation': 'Operation', 'component.status.success': 'Successfully', 'component.status.fail': 'Failed', diff --git a/web/src/locales/zh-CN/component.ts b/web/src/locales/zh-CN/component.ts index d0829c6231..210dd864d3 100644 --- a/web/src/locales/zh-CN/component.ts +++ b/web/src/locales/zh-CN/component.ts @@ -28,6 +28,7 @@ export default { 'component.global.add': '新建', 'component.global.save': '保存', 'component.global.edit': '编辑', + 'component.global.manage': '管理', 'component.global.update': '更新', 'component.global.get': '获取', 'component.global.edit.plugin': '编辑插件', @@ -35,6 +36,7 @@ export default { 'component.global.list': '列表', 'component.global.description': '描述', 'component.global.labels': '标签', + 'component.global.version': '版本', 'component.global.operation': '操作', 'component.status.success': '成功', 'component.status.fail': '失败', diff --git a/web/src/pages/Consumer/Create.tsx b/web/src/pages/Consumer/Create.tsx index 2bfdfe4f74..217c335e31 100644 --- a/web/src/pages/Consumer/Create.tsx +++ b/web/src/pages/Consumer/Create.tsx @@ -29,7 +29,7 @@ import { fetchItem, create, update, fetchPlugList } from './service'; const Page: React.FC = (props) => { const [step, setStep] = useState(1); const [plugins, setPlugins] = useState({}); - const [pluginList, setPluginList] = useState([]) + const [pluginList, setPluginList] = useState([]); const [form1] = Form.useForm(); const { formatMessage } = useIntl(); @@ -52,12 +52,13 @@ const Page: React.FC = (props) => { (username ? update(username, data) : create(data)) .then(() => { notification.success({ - message: `${username - ? formatMessage({ id: 'component.global.edit' }) - : formatMessage({ id: 'component.global.create' }) - } ${formatMessage({ id: 'menu.consumer' })} ${formatMessage({ - id: 'component.status.success', - })}`, + message: `${ + username + ? formatMessage({ id: 'component.global.edit' }) + : formatMessage({ id: 'component.global.create' }) + } ${formatMessage({ id: 'menu.consumer' })} ${formatMessage({ + id: 'component.status.success', + })}`, }); history.push('/consumer/list'); }) @@ -76,7 +77,7 @@ const Page: React.FC = (props) => { if ( !Object.keys(plugins).filter( (name) => - (pluginList.find(item => item.name === name)!.type === 'auth') && + pluginList.find((item) => item.name === name)!.type === 'auth' && !plugins[name].disable, ).length ) { @@ -98,10 +99,11 @@ const Page: React.FC = (props) => { return ( <> diff --git a/web/src/pages/Plugin/PluginMarket.tsx b/web/src/pages/Plugin/PluginMarket.tsx index 3613e00a1f..44032ec341 100644 --- a/web/src/pages/Plugin/PluginMarket.tsx +++ b/web/src/pages/Plugin/PluginMarket.tsx @@ -48,9 +48,9 @@ const PluginMarket: React.FC = () => { ...pluginsData, }, }).then(() => { - // TODO: + // TODO: window.location.reload(); - }) + }); }} /> diff --git a/web/src/pages/Plugin/components/Step1.tsx b/web/src/pages/Plugin/components/Step1.tsx deleted file mode 100644 index 2944f98194..0000000000 --- a/web/src/pages/Plugin/components/Step1.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - */ diff --git a/web/src/pages/Plugin/typing.d.ts b/web/src/pages/Plugin/typing.d.ts index f0f3ac9caf..6f9c31b9a6 100644 --- a/web/src/pages/Plugin/typing.d.ts +++ b/web/src/pages/Plugin/typing.d.ts @@ -25,5 +25,5 @@ declare namespace PluginModule { type GlobalRule = { id: string; plugins: Record; - } + }; } diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx index a3d5f5307d..ab1257f130 100644 --- a/web/src/pages/Route/Create.tsx +++ b/web/src/pages/Route/Create.tsx @@ -112,8 +112,8 @@ const Page: React.FC = (props) => { if (action === 'advancedMatchingRulesChange') { setAdvancedMatchingRules(data); } - if (action === 'labelsChange') { - form1.setFieldsValue({ ...form1.getFieldsValue(), labels: data }); + if (action === 'custom_normal_labels') { + form1.setFieldsValue({ custom_normal_labels: data }); } }} isEdit={props.route.path.indexOf('edit') > 0} diff --git a/web/src/pages/Route/List.tsx b/web/src/pages/Route/List.tsx index 304ad3b179..4a6c8a63dc 100644 --- a/web/src/pages/Route/List.tsx +++ b/web/src/pages/Route/List.tsx @@ -103,11 +103,13 @@ const Page: React.FC = () => { title: formatMessage({ id: 'component.global.labels' }), dataIndex: 'labels', render: (_, record) => { - return Object.keys(record.labels || {}).map((item) => ( - - {item}:{record.labels[item]} - - )); + return Object.keys(record.labels || {}) + .filter((item) => item !== 'API_VERSION') + .map((item) => ( + + {item}:{record.labels[item]} + + )); }, renderFormItem: (_, { type }) => { if (type === 'form') { @@ -127,18 +129,53 @@ const Page: React.FC = () => { ); }} > - {Object.keys(labelList).map((key) => { - return ( - - {(labelList[key] || []).map((value: string) => ( - - ))} - - ); - })} + {Object.keys(labelList) + .filter((item) => item !== 'API_VERSION') + .map((key) => { + return ( + + {(labelList[key] || []).map((value: string) => ( + + ))} + + ); + })} + + ); + }, + }, + { + title: formatMessage({ id: 'component.global.version' }), + dataIndex: 'API_VERSION', + render: (_, record) => { + return Object.keys(record.labels || {}) + .filter((item) => item === 'API_VERSION') + .map((item) => record.labels[item]); + }, + renderFormItem: (_, { type }) => { + if (type === 'form') { + return null; + } + + return ( + ); }, @@ -169,36 +206,36 @@ const Page: React.FC = () => { render: (_, record) => ( <> - + ) : null} + {record.status ? ( + { + handlePublishOffline(record.id, RouteStatus.Offline); + }} + okButtonProps={{ + danger: true, + }} + okText={formatMessage({ id: 'component.global.confirm' })} + cancelText={formatMessage({ id: 'component.global.cancel' })} + > + + + ) : null} + - - { - handlePublishOffline(record.id, RouteStatus.Offline); - }} - okText={formatMessage({ id: 'component.global.confirm' })} - cancelText={formatMessage({ id: 'component.global.cancel' })} - disabled={Boolean(!record.status)} - > - - { @@ -210,6 +247,9 @@ const Page: React.FC = () => { ); }); }} + okButtonProps={{ + danger: true, + }} okText={formatMessage({ id: 'component.global.confirm' })} cancelText={formatMessage({ id: 'component.global.cancel' })} > diff --git a/web/src/pages/Route/components/Step1/LabelsDrawer.tsx b/web/src/pages/Route/components/Step1/LabelsDrawer.tsx index ba5b221e46..720a437fc8 100644 --- a/web/src/pages/Route/components/Step1/LabelsDrawer.tsx +++ b/web/src/pages/Route/components/Step1/LabelsDrawer.tsx @@ -22,16 +22,20 @@ import { useIntl } from 'umi'; import { transformLableValueToKeyValue } from '../../transform'; import { fetchLabelList } from '../../service'; -interface Props extends Pick { - labelsDataSource: string[]; +type Props = { + title?: string; + actionName: string; + dataSource: string[]; disabled: boolean; onClose(): void; -} +} & Pick; const LabelList = (disabled: boolean, labelList: RouteModule.LabelList) => { const { formatMessage } = useIntl(); - const keyOptions = Object.keys(labelList || {}).map((item) => ({ value: item })); + const keyOptions = Object.keys(labelList || {}) + .filter((item) => item !== 'API_VERSION') + .map((item) => ({ value: item })); return ( {(fields, { add, remove }) => { @@ -108,12 +112,14 @@ const LabelList = (disabled: boolean, labelList: RouteModule.LabelList) => { }; const LabelsDrawer: React.FC = ({ - disabled, - labelsDataSource, + title = 'Label Manager', + actionName = '', + disabled = false, + dataSource = [], onClose, onChange = () => {}, }) => { - const transformLabel = transformLableValueToKeyValue(labelsDataSource); + const transformLabel = transformLableValueToKeyValue(dataSource); const { formatMessage } = useIntl(); const [form] = Form.useForm(); @@ -126,12 +132,13 @@ const LabelsDrawer: React.FC = ({ return ( @@ -152,7 +159,7 @@ const LabelsDrawer: React.FC = ({ } onChange({ - action: 'labelsChange', + action: actionName, data, }); onClose(); diff --git a/web/src/pages/Route/components/Step1/MetaView.tsx b/web/src/pages/Route/components/Step1/MetaView.tsx index 25e1f31162..09f64e550e 100644 --- a/web/src/pages/Route/components/Step1/MetaView.tsx +++ b/web/src/pages/Route/components/Step1/MetaView.tsx @@ -14,38 +14,94 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import Form from 'antd/es/form'; -import { Input, Switch, Select, Button, Tag } from 'antd'; +import { Input, Switch, Select, Button, Tag, AutoComplete } from 'antd'; import { useIntl } from 'umi'; import { PanelSection } from '@api7-dashboard/ui'; import { FORM_ITEM_WITHOUT_LABEL } from '@/pages/Route/constants'; import LabelsDrawer from './LabelsDrawer'; +import { fetchLabelList } from '../../service'; const MetaView: React.FC = ({ disabled, form, isEdit, onChange }) => { const { formatMessage } = useIntl(); const [visible, setVisible] = useState(false); + const [labelList, setLabelList] = useState({}); - return ( - - {visible && ( - - {() => { - if (form.getFieldValue('labels')) { + useEffect(() => { + // TODO: use a better state name + fetchLabelList().then(setLabelList); + }, []); + + const NormalLabelComponent = () => { + const field = 'custom_normal_labels'; + const title = 'Label Manager'; + + return ( + + + { - const { value, closable, onClose } = props; - return ( - - {value} - - ); - }} - /> - - - - + + + + + request(`/routes/${rid}`).then((data) => transformRouteData(data.data)); export const fetchList = ({ current = 1, pageSize = 10, ...res }) => { - const { labels } = res; + const { labels, API_VERSION } = res; return request>>('/routes', { params: { name: res.name, uri: res.uri, - label: (labels || []).join(','), + label: (labels || []).concat(API_VERSION).join(','), page: current, page_size: pageSize, }, diff --git a/web/src/pages/Route/transform.ts b/web/src/pages/Route/transform.ts index 401b15d17a..5b10a8cdb7 100644 --- a/web/src/pages/Route/transform.ts +++ b/web/src/pages/Route/transform.ts @@ -25,12 +25,15 @@ export const transformLableValueToKeyValue = (data: string[]) => { }); }; +// Transform Route data then sent to API export const transformStepData = ({ form1Data, form2Data, advancedMatchingRules, step3Data, }: RouteModule.RequestData) => { + const { custom_normal_labels, custom_version_label, service_id = '' } = form1Data; + let redirect: RouteModule.Redirect = {}; const step3DataCloned = cloneDeep(step3Data); if (form1Data.redirectOption === 'disabled') { @@ -44,14 +47,16 @@ export const transformStepData = ({ }; } - const labels = {}; - transformLableValueToKeyValue(form1Data.labels).forEach((item) => { - labels[item.labelKey] = item.labelValue; + const labels: Record = {}; + transformLableValueToKeyValue(custom_normal_labels).forEach(({ labelKey, labelValue }) => { + labels[labelKey] = labelValue; }); - const { service_id = '' } = form1Data; + if (custom_version_label) { + labels.API_VERSION = custom_version_label; + } const data: Partial = { - ...omit(form1Data, 'labels'), + ...form1Data, labels, ...step3DataCloned, vars: advancedMatchingRules.map((rule) => { @@ -81,7 +86,7 @@ export const transformStepData = ({ } if (redirect.http_to_https) { - if (Object.keys(data.plugins!).length === 0) { + if (Object.keys(data.plugins || {}).length === 0) { data.plugins = {}; } data.plugins!.redirect = redirect; @@ -92,6 +97,8 @@ export const transformStepData = ({ // Remove some of the frontend custom variables return omit(data, [ + 'custom_version_label', + 'custom_normal_labels', 'advancedMatchingRules', 'upstreamHostList', 'upstreamPath', @@ -100,8 +107,8 @@ export const transformStepData = ({ 'ret_code', 'redirectOption', service_id.length === 0 ? 'service_id' : '', - !Object.keys(step3DataCloned.plugins || {}).length ? 'plugins' : '', - !Object.keys(step3DataCloned.script || {}).length ? 'script' : '', + !Object.keys(data.plugins || {}).length ? 'plugins' : '', + !Object.keys(data.script || {}).length ? 'script' : '', form1Data.hosts.filter(Boolean).length === 0 ? 'hosts' : '', form1Data.redirectOption === 'disabled' ? 'redirect' : '', data.remote_addrs?.filter(Boolean).length === 0 ? 'remote_addrs' : '', @@ -123,6 +130,7 @@ export const transformStepData = ({ service_id.length !== 0 ? 'service_id' : '', form1Data.hosts.filter(Boolean).length !== 0 ? 'hosts' : '', data.remote_addrs?.filter(Boolean).length !== 0 ? 'remote_addrs' : '', + form1Data.custom_version_label.length !== 0 ? 'labels' : '', ]); }; @@ -154,11 +162,12 @@ export const transformUpstreamNodes = ( return data; }; +// Transform response's data export const transformRouteData = (data: RouteModule.Body) => { const { name, desc, - labels, + labels = {}, methods = [], uris, uri, @@ -173,6 +182,7 @@ export const transformRouteData = (data: RouteModule.Body) => { priority = 0, enable_websocket, } = data; + const form1Data: Partial = { name, desc, @@ -180,7 +190,11 @@ export const transformRouteData = (data: RouteModule.Body) => { hosts: hosts || (host && [host]) || [''], uris: uris || (uri && [uri]) || [], remote_addrs: remote_addrs || [''], - labels: Object.keys(labels || []).map((item) => `${item}:${labels[item]}`), + // NOTE: API_VERSION is a system label + custom_version_label: labels.API_VERSION || '', + custom_normal_labels: Object.keys(labels) + .filter((item) => item !== 'API_VERSION') + .map((key) => `${key}:${labels[key]}`), // @ts-ignore methods: methods.length ? methods : ['ALL'], priority, diff --git a/web/src/pages/Route/typing.d.ts b/web/src/pages/Route/typing.d.ts index 6b87a0b512..60afe5f87b 100644 --- a/web/src/pages/Route/typing.d.ts +++ b/web/src/pages/Route/typing.d.ts @@ -83,7 +83,7 @@ declare namespace RouteModule { remote_addrs: string[]; vars: [string, Operator, string][]; upstream: { - type: 'roundrobin' | 'chash'; + type: 'roundrobin' | 'chash' | 'ewma'; hash_on?: string; key?: string; nodes: { @@ -134,16 +134,14 @@ declare namespace RouteModule { advancedMatchingRules: MatchingRule[]; disabled?: boolean; isEdit?: boolean; - onChange?(data: { - action: 'redirectOptionChange' | 'advancedMatchingRulesChange' | 'labelsChange'; - data: T; - }): void; + onChange?(data: { action: string; data: T }): void; }; type Form1Data = { name: string; desc: string; - labels: string[]; + custom_version_label: string; + custom_normal_labels: string[]; priority: number; websocket: boolean; hosts: string[];