This is an automated email from the ASF dual-hosted git repository.
sunyi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new d4792fc feat: add route group (#999)
d4792fc is described below
commit d4792fcf4eea985ea44a76f86e93d5c784d2ae5c
Author: litesun <[email protected]>
AuthorDate: Fri Dec 25 16:36:55 2020 +0800
feat: add route group (#999)
* feat: added route label search
* fix: react key warning
* feat: add label drawer
* feat: add save and cancel button in drawer
* feat: added edit feature in labelDrawer
* fix: lost click event when div child is empty
* feat: update route transform with labels
* feat: add AutoComplete
* feat: use form.list render labels
* feat: clean code
* fix: warning when click save button
* feat: update fetchLabelList
* feat: update create route
* feat: update typing
* Merge branch master into feat-route-group-1
* Update component.ts
* feat: update code
* feat: update i18n
Co-authored-by: 琚致远 <[email protected]>
---
web/src/locales/en-US/component.ts | 1 +
web/src/locales/zh-CN/component.ts | 1 +
web/src/pages/Route/Create.tsx | 12 +-
web/src/pages/Route/List.tsx | 61 +++++++-
.../pages/Route/components/Step1/LabelsDrawer.tsx | 174 +++++++++++++++++++++
web/src/pages/Route/components/Step1/MetaView.tsx | 50 +++++-
web/src/pages/Route/constants.ts | 1 +
web/src/pages/Route/service.ts | 13 +-
web/src/pages/Route/transform.ts | 39 ++++-
web/src/pages/Route/typing.d.ts | 15 +-
10 files changed, 349 insertions(+), 18 deletions(-)
diff --git a/web/src/locales/en-US/component.ts
b/web/src/locales/en-US/component.ts
index ac63595..a712701 100644
--- a/web/src/locales/en-US/component.ts
+++ b/web/src/locales/en-US/component.ts
@@ -32,6 +32,7 @@ export default {
'component.global.loading': 'Loading',
'component.global.list': 'List',
'component.global.description': 'Description',
+ 'component.global.labels': 'Labels',
'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 a074362..b62ba40 100644
--- a/web/src/locales/zh-CN/component.ts
+++ b/web/src/locales/zh-CN/component.ts
@@ -32,6 +32,7 @@ export default {
'component.global.loading': '加载中',
'component.global.list': '列表',
'component.global.description': '描述',
+ 'component.global.labels': '标签',
'component.global.operation': '操作',
'component.status.success': '成功',
'component.status.fail': '失败',
diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx
index 5755505..4fcc921 100644
--- a/web/src/pages/Route/Create.tsx
+++ b/web/src/pages/Route/Create.tsx
@@ -112,6 +112,9 @@ const Page: React.FC<Props> = (props) => {
if (action === 'advancedMatchingRulesChange') {
setAdvancedMatchingRules(data);
}
+ if (action === 'labelsChange') {
+ form1.setFieldsValue({ ...form1.getFieldsValue(), labels: data })
+ }
}}
isEdit={props.route.path.indexOf('edit') > 0}
/>
@@ -253,11 +256,10 @@ const Page: React.FC<Props> = (props) => {
return (
<>
<PageHeaderWrapper
- title={`${
- (props as any).match.params.rid
- ? formatMessage({ id: 'component.global.edit' })
- : formatMessage({ id: 'component.global.create' })
- } ${formatMessage({ id: 'menu.routes' })}`}
+ title={`${(props as any).match.params.rid
+ ? formatMessage({ id: 'component.global.edit' })
+ : formatMessage({ id: 'component.global.create' })
+ } ${formatMessage({ id: 'menu.routes' })}`}
>
<Card bordered={false}>
<Steps current={step - 1} className={styles.steps}>
diff --git a/web/src/pages/Route/List.tsx b/web/src/pages/Route/List.tsx
index 30979fc..c131899 100644
--- a/web/src/pages/Route/List.tsx
+++ b/web/src/pages/Route/List.tsx
@@ -14,21 +14,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React, { useRef, useState } from 'react';
+import React, { useRef, useEffect, useState } from 'react';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
-import { Button, Popconfirm, notification, Tag, Space } from 'antd';
+import { Button, Popconfirm, notification, Tag, Space, Select } from 'antd';
import { history, useIntl } from 'umi';
import { PlusOutlined, BugOutlined } from '@ant-design/icons';
-import { timestampToLocaleString } from '@/helpers';
-import { fetchList, remove, updateRouteStatus } from './service';
+import { timestampToLocaleString } from '@/helpers';
+import { fetchList, remove, fetchLabelList, updateRouteStatus } from
'./service';
import { DebugDrawView } from './components/DebugViews';
+
+const { OptGroup, Option } = Select;
+
const Page: React.FC = () => {
const ref = useRef<ActionType>();
const { formatMessage } = useIntl();
+ const [labelList, setLabelList] = useState<RouteModule.LabelList>({});
+
+ useEffect(() => {
+ fetchLabelList().then(setLabelList);
+ }, []);
enum RouteStatus {
Offline = 0,
Publish,
@@ -91,6 +99,47 @@ const Page: React.FC = () => {
hideInSearch: true,
},
{
+ title: formatMessage({ id: 'component.global.labels' }),
+ dataIndex: 'labels',
+ render: (_, record) => {
+ return Object.keys(record.labels || {}).map((item) => (
+ <Tag key={Math.random().toString(36).slice(2)}>
+ {item}:{record.labels[item]}
+ </Tag>
+ ));
+ },
+ renderFormItem: (_, { type }) => {
+ if (type === 'form') {
+ return null;
+ }
+
+ return (
+ <Select
+ mode="tags"
+ style={{ width: '100%' }}
+ tagRender={(props) => {
+ const { value, closable, onClose } = props;
+ return (
+ <Tag closable={closable} onClose={onClose} style={{
marginRight: 3 }}>
+ {value}
+ </Tag>
+ );
+ }}
+ >
+ {Object.keys(labelList).map((key) => {
+ return (
+ <OptGroup label={key}
key={Math.random().toString(36).slice(2)}>
+ {(labelList[key] || []).map((value: string) => (
+ <Option key={Math.random().toString(36).slice(2)}
value={`${key}:${value}`}> {value} </Option>
+ ))}
+ </OptGroup>
+ );
+ })}
+ </Select>
+ );
+ }
+ },
+ {
title: formatMessage({ id: 'page.route.status' }),
dataIndex: 'status',
render: (_, record) => (
@@ -98,8 +147,8 @@ const Page: React.FC = () => {
{record.status ? (
<Tag color="green">{formatMessage({ id: 'page.route.published'
})}</Tag>
) : (
- <Tag color="red">{formatMessage({ id: 'page.route.unpublished'
})}</Tag>
- )}
+ <Tag color="red">{formatMessage({ id: 'page.route.unpublished'
})}</Tag>
+ )}
</>
),
},
diff --git a/web/src/pages/Route/components/Step1/LabelsDrawer.tsx
b/web/src/pages/Route/components/Step1/LabelsDrawer.tsx
new file mode 100644
index 0000000..6fb0a70
--- /dev/null
+++ b/web/src/pages/Route/components/Step1/LabelsDrawer.tsx
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ */
+import React, { useEffect, useState } from 'react';
+import { AutoComplete, Button, Col, Drawer, Form, notification, Row } from
'antd';
+import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
+import { useIntl } from 'umi';
+
+import { transformLableValueToKeyValue } from '../../transform';
+import { fetchLabelList } from '../../service';
+
+interface Props extends Pick<RouteModule.Step1PassProps, 'onChange'> {
+ labelsDataSource: string[];
+ disabled: boolean;
+ onClose(): void;
+}
+
+const LabelList = (disabled: boolean, labelList: RouteModule.LabelList) => {
+ const { formatMessage } = useIntl();
+
+ const keyOptions = Object.keys(labelList || {}).map((item) => ({ value: item
}));
+ return (
+ <Form.List name="labels">
+ {(fields, { add, remove }) => {
+ return (
+ <>
+ {fields.map((field, index) => (
+ <Form.Item
+ key={field.key}
+ label={index === 0 && 'Label'}
+ labelCol={{ span: index === 0 ? 3 : 0 }}
+ wrapperCol={{ offset: index === 0 ? 0 : 3 }}
+ >
+ <Row style={{ marginBottom: 10 }} gutter={16}>
+ <Col>
+ <Form.Item
+ style={{ marginBottom: 0 }}
+ name={[field.name, 'labelKey']}
+ rules={[
+ {
+ required: true,
+ message: 'Please input key',
+ },
+ ]}
+ >
+ <AutoComplete options={keyOptions} style={{ width: 100
}} />
+ </Form.Item>
+ </Col>
+ <Col>
+ <Form.Item shouldUpdate noStyle>
+ {({ getFieldValue }) => {
+ const key = getFieldValue(['labels', field.name,
'labelKey']);
+ let valueOptions = [{ value: '' }];
+ if (labelList) {
+ valueOptions = (labelList[key] || []).map((item) =>
({ value: item }));
+ }
+
+ return (
+ <Form.Item
+ noStyle
+ name={[field.name, 'labelValue']}
+ fieldKey={[field.fieldKey, 'labelValue']}
+ rules={[
+ {
+ required: true,
+ message: 'Please input value',
+ },
+ ]}
+ >
+ <AutoComplete options={valueOptions} style={{
width: 100 }} />
+ </Form.Item>
+ );
+ }}
+ </Form.Item>
+ </Col>
+ <Col>
+ {!disabled && <MinusCircleOutlined onClick={() =>
remove(field.name)} />}
+ </Col>
+ </Row>
+ </Form.Item>
+ ))}
+ {!disabled && (
+ <Form.Item wrapperCol={{ offset: 3 }}>
+ <Button type="dashed" onClick={add}>
+ <PlusOutlined />
+ {formatMessage({ id: 'component.global.add' })}
+ </Button>
+ </Form.Item>
+ )}
+ </>
+ );
+ }}
+ </Form.List>
+ );
+};
+
+const LabelsDrawer: React.FC<Props> = ({
+ disabled,
+ labelsDataSource,
+ onClose,
+ onChange = () => { },
+}) => {
+ const transformLabel = transformLableValueToKeyValue(labelsDataSource);
+
+ const { formatMessage } = useIntl();
+ const [form] = Form.useForm();
+ const [labelList, setLabelList] = useState<RouteModule.LabelList>({});
+ form.setFieldsValue({ labels: transformLabel });
+
+ useEffect(() => {
+ fetchLabelList().then(setLabelList);
+ }, []);
+
+ return (
+ <Drawer
+ title="Edit labels"
+ placement="right"
+ width={512}
+ visible
+ closable
+ onClose={onClose}
+ footer={
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <Button onClick={onClose}>{formatMessage({ id:
'component.global.cancel' })}</Button>
+ <Button
+ type="primary"
+ style={{ marginRight: 8, marginLeft: 8 }}
+ onClick={(e) => {
+ e.persist();
+ form.validateFields().then(({ labels }) => {
+ const data = labels.map((item: any) =>
`${item.labelKey}:${item.labelValue}`)
+ // check for duplicates
+ if (new Set(data).size !== data.length) {
+ notification.warning({
+ message: `Config Error`,
+ description: 'Please do not enter duplicate labels',
+ });
+ return;
+ }
+
+ onChange({
+ action: 'labelsChange',
+ data,
+ });
+ onClose();
+ });
+ }}
+ >
+ {formatMessage({ id: 'component.global.confirm' })}
+ </Button>
+ </div >
+ }
+ >
+ <Form form={form} layout="horizontal">
+ {LabelList(disabled, labelList || {})}
+ </Form>
+ </Drawer >
+ );
+};
+
+export default LabelsDrawer;
diff --git a/web/src/pages/Route/components/Step1/MetaView.tsx
b/web/src/pages/Route/components/Step1/MetaView.tsx
index a7d86eb..8234be9 100644
--- a/web/src/pages/Route/components/Step1/MetaView.tsx
+++ b/web/src/pages/Route/components/Step1/MetaView.tsx
@@ -14,17 +14,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React from 'react';
+import React, { useState } from 'react';
import Form from 'antd/es/form';
-import { Input, Switch } from 'antd';
+import { Input, Switch, Select, Button, Tag } from 'antd';
import { useIntl } from 'umi';
import { PanelSection } from '@api7-dashboard/ui';
-const MetaView: React.FC<RouteModule.Step1PassProps> = ({ disabled, isEdit })
=> {
+import { FORM_ITEM_WITHOUT_LABEL } from '@/pages/Route/constants';
+import LabelsDrawer from './LabelsDrawer';
+
+const MetaView: React.FC<RouteModule.Step1PassProps> = ({ disabled, form,
isEdit, onChange, }) => {
const { formatMessage } = useIntl();
+ const [visible, setVisible] = useState(false);
return (
<PanelSection title={formatMessage({ id:
'page.route.panelSection.title.nameDescription' })}>
+ {visible && (
+ <Form.Item shouldUpdate noStyle>
+ {() => {
+ if (form.getFieldValue('labels')) {
+ return (
+ <LabelsDrawer
+ labelsDataSource={form.getFieldValue('labels')}
+ disabled={disabled || false}
+ onChange={onChange}
+ onClose={() => setVisible(false)}
+ />
+ );
+ }
+ return null;
+ }}
+ </Form.Item>
+ )}
<Form.Item
label={formatMessage({ id: 'component.global.name' })}
name="name"
@@ -49,6 +70,29 @@ const MetaView: React.FC<RouteModule.Step1PassProps> = ({
disabled, isEdit }) =>
disabled={disabled}
/>
</Form.Item>
+ <Form.Item label={formatMessage({ id: 'component.global.labels' })}
name="labels">
+ <Select
+ mode="tags"
+ style={{ width: '100%' }}
+ placeholder="--"
+ disabled={disabled}
+ open={false}
+ bordered={false}
+ tagRender={(props) => {
+ const { value, closable, onClose } = props;
+ return (
+ <Tag closable={closable && !disabled} onClose={onClose} style={{
marginRight: 3 }}>
+ {value}
+ </Tag>
+ );
+ }}
+ />
+ </Form.Item>
+ <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
+ <Button disabled={disabled} onClick={() => setVisible(true)}>
+ {formatMessage({ id: 'component.global.edit' })}
+ </Button>
+ </Form.Item>
<Form.Item label={formatMessage({ id: 'component.global.description' })}
name="desc">
<Input.TextArea
placeholder={formatMessage({ id:
'component.global.input.placeholder.description' })}
diff --git a/web/src/pages/Route/constants.ts b/web/src/pages/Route/constants.ts
index 1082e5e..ab6c30c 100644
--- a/web/src/pages/Route/constants.ts
+++ b/web/src/pages/Route/constants.ts
@@ -43,6 +43,7 @@ export const FORM_ITEM_WITHOUT_LABEL = {
export const DEFAULT_STEP_1_DATA: RouteModule.Form1Data = {
name: '',
desc: '',
+ labels:[],
status: 1,
priority: 0,
websocket: false,
diff --git a/web/src/pages/Route/service.ts b/web/src/pages/Route/service.ts
index 12e2df1..099428b 100644
--- a/web/src/pages/Route/service.ts
+++ b/web/src/pages/Route/service.ts
@@ -17,7 +17,12 @@
import { request } from 'umi';
import { pickBy, identity } from 'lodash';
-import { transformStepData, transformRouteData, transformUpstreamNodes } from
'./transform';
+import {
+ transformStepData,
+ transformRouteData,
+ transformUpstreamNodes,
+ transformLabelList
+} from './transform';
export const create = (data: RouteModule.RequestData) =>
request(`/routes`, {
@@ -32,13 +37,15 @@ export const update = (rid: number, data:
RouteModule.RequestData) =>
});
export const fetchItem = (rid: number) =>
- request(`/routes/${rid}`).then((data) => transformRouteData(data.data));
+ request(`/routes/${rid}`).then((data) => (transformRouteData(data.data)));
export const fetchList = ({ current = 1, pageSize = 10, ...res }) => {
+ const { labels } = res;
return request<Res<ResListData<RouteModule.ResponseBody>>>('/routes', {
params: {
name: res.name,
uri: res.uri,
+ label: (labels || []).join(','),
page: current,
page_size: pageSize,
},
@@ -86,6 +93,8 @@ export const checkHostWithSSL = (hosts: string[]) =>
data: hosts,
});
+export const fetchLabelList = () =>
+ request('/labels/route').then(({ data }) => ((transformLabelList(data.rows))
as RouteModule.LabelList));
export const updateRouteStatus = (rid: string, status:
RouteModule.RouteStatus) =>
request(`/routes/${rid}`, {
diff --git a/web/src/pages/Route/transform.ts b/web/src/pages/Route/transform.ts
index 3c3de56..c7b442d 100644
--- a/web/src/pages/Route/transform.ts
+++ b/web/src/pages/Route/transform.ts
@@ -16,6 +16,15 @@
*/
import { omit, pick, cloneDeep } from 'lodash';
+export const transformLableValueToKeyValue = (data: string[]) => {
+ return (data || []).map((item) => {
+ const index = item.indexOf(':');
+ const labelKey = item.substring(0, index);
+ const labelValue = item.substring(index + 1);
+ return { labelKey, labelValue, key: Math.random().toString(36).slice(2) };
+ });
+};
+
export const transformStepData = ({
form1Data,
form2Data,
@@ -35,10 +44,15 @@ export const transformStepData = ({
};
}
+ const labels = {};
+ transformLableValueToKeyValue(form1Data.labels).forEach(item => {
+ labels[item.labelKey] = item.labelValue;
+ })
const { service_id = '' } = form1Data;
const data: Partial<RouteModule.Body> = {
- ...form1Data,
+ ...omit(form1Data, 'labels'),
+ labels,
...step3DataCloned,
vars: advancedMatchingRules.map((rule) => {
const { operator, position, name, value } = rule;
@@ -144,6 +158,7 @@ export const transformRouteData = (data: RouteModule.Body)
=> {
const {
name,
desc,
+ labels,
methods = [],
uris,
uri,
@@ -165,6 +180,7 @@ 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]}`),
// @ts-ignore
methods: methods.length ? methods : ["ALL"],
priority,
@@ -201,3 +217,24 @@ export const transformRouteData = (data: RouteModule.Body)
=> {
advancedMatchingRules,
};
};
+
+export const transformLabelList = (data: RouteModule.ResponseLabelList) => {
+ if (!data) {
+ return {};
+ }
+ const transformData = {};
+ data.forEach((item) => {
+ const key = Object.keys(item)[0];
+ const value = item[key];
+ if (!transformData[key]) {
+ transformData[key] = [];
+ transformData[key].push(value);
+ return;
+ }
+
+ if (transformData[key] && !transformData[key][value]) {
+ transformData[key].push(value);
+ }
+ });
+ return transformData;
+};
diff --git a/web/src/pages/Route/typing.d.ts b/web/src/pages/Route/typing.d.ts
index 0363b32..335e9dc 100644
--- a/web/src/pages/Route/typing.d.ts
+++ b/web/src/pages/Route/typing.d.ts
@@ -72,6 +72,7 @@ declare namespace RouteModule {
id?: number;
status: number;
name: string;
+ labels: Record<string, string>;
desc: string;
priority?: number;
methods: HttpMethod[];
@@ -118,13 +119,23 @@ declare namespace RouteModule {
key: string;
}
+ type ResponseLabelList = Record<string, string>[];
+
+ type LabelList = Record<string, string[]>;
+
+ type LabelTableProps = {
+ labelKey: string,
+ labelValue: string,
+ key: string
+ }
+
type Step1PassProps = {
form: FormInstance;
advancedMatchingRules: MatchingRule[];
disabled?: boolean;
isEdit?: boolean;
onChange?(data: {
- action: 'redirectOptionChange' | 'advancedMatchingRulesChange';
+ action: 'redirectOptionChange' | 'advancedMatchingRulesChange' |
'labelsChange';
data: T;
}): void;
};
@@ -132,6 +143,7 @@ declare namespace RouteModule {
type Form1Data = {
name: string;
desc: string;
+ labels: string[];
priority: number;
websocket: boolean;
hosts: string[];
@@ -215,6 +227,7 @@ declare namespace RouteModule {
remote_addrs: string[];
script: any;
desc?: string;
+ labels: Record<string, string>;
upstream: {
checks: UpstreamModule.HealthCheck;
create_time: number;