You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by su...@apache.org on 2021/03/22 04:06:32 UTC
[apisix-dashboard] branch master updated: feat: add rawDataEditor
edit mode (#1601)
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 548bbe3 feat: add rawDataEditor edit mode (#1601)
548bbe3 is described below
commit 548bbe38f66ff18a6de6567bf13dc5b81a995a46
Author: litesun <su...@apache.org>
AuthorDate: Mon Mar 22 12:06:26 2021 +0800
feat: add rawDataEditor edit mode (#1601)
---
web/cypress/fixtures/rawDataEditor-dataset.json | 86 ++++++++++++++
web/cypress/fixtures/selector.json | 1 +
.../rawDataEditor/test-rawDataEditor.spec.js | 96 ++++++++++++++++
web/src/components/RawDataEditor/RawDataEditor.tsx | 124 +++++++++++++++++++--
web/src/constants.ts | 2 +
web/src/locales/en-US/component.ts | 1 +
web/src/locales/zh-CN/component.ts | 1 +
web/src/pages/Consumer/List.tsx | 37 ++++--
web/src/pages/Route/List.tsx | 37 ++++--
.../Route/components/Step1/MatchingRulesView.tsx | 1 +
web/src/pages/Route/service.ts | 8 +-
web/src/pages/Route/typing.d.ts | 4 +-
web/src/pages/Service/List.tsx | 37 ++++--
web/src/pages/Upstream/List.tsx | 39 +++++--
14 files changed, 430 insertions(+), 44 deletions(-)
diff --git a/web/cypress/fixtures/rawDataEditor-dataset.json b/web/cypress/fixtures/rawDataEditor-dataset.json
new file mode 100644
index 0000000..613322f
--- /dev/null
+++ b/web/cypress/fixtures/rawDataEditor-dataset.json
@@ -0,0 +1,86 @@
+{
+ "Route": {
+ "uris": [
+ "/*"
+ ],
+ "name": "root",
+ "methods": [
+ "GET",
+ "HEAD",
+ "POST",
+ "PUT",
+ "DELETE",
+ "OPTIONS",
+ "PATCH"
+ ],
+ "upstream": {
+ "nodes": [
+ {
+ "host": "12.12.12.12",
+ "port": 80,
+ "weight": 1
+ }
+ ],
+ "timeout": {
+ "connect": 6000,
+ "read": 6000,
+ "send": 6000
+ },
+ "type": "roundrobin",
+ "pass_host": "pass"
+ },
+ "status": 1
+ },
+ "Upstream": {
+ "nodes": [
+ {
+ "host": "12.12.12.12",
+ "port": 80,
+ "weight": 1
+ }
+ ],
+ "timeout": {
+ "connect": 6,
+ "read": 6,
+ "send": 6
+ },
+ "type": "roundrobin",
+ "pass_host": "pass",
+ "name": "test"
+ },
+ "Service": {
+ "name": "test",
+ "upstream": {
+ "nodes": [
+ {
+ "host": "12.12.12.12",
+ "port": 80,
+ "weight": 1
+ }
+ ],
+ "timeout": {
+ "connect": 6,
+ "read": 6,
+ "send": 6
+ },
+ "type": "roundrobin",
+ "pass_host": "pass"
+ },
+ "plugins": {
+ "jwt-auth": {
+ "disable": false
+ }
+ }
+ },
+ "Consumer": {
+ "username": "test",
+ "desc": "desc",
+ "plugins": {
+ "basic-auth": {
+ "disable": false,
+ "password": "test",
+ "username": "test"
+ }
+ }
+ }
+}
diff --git a/web/cypress/fixtures/selector.json b/web/cypress/fixtures/selector.json
index 121adf1..0e1c8d3 100644
--- a/web/cypress/fixtures/selector.json
+++ b/web/cypress/fixtures/selector.json
@@ -13,6 +13,7 @@
"pluginCard": ".ant-card",
"pageContent": ".ant-pro-page-container",
+ "tableBody":".ant-table-tbody",
"tableCell": ".ant-table-cell",
"empty": ".ant-empty-normal",
"refresh": ".anticon-reload",
diff --git a/web/cypress/integration/rawDataEditor/test-rawDataEditor.spec.js b/web/cypress/integration/rawDataEditor/test-rawDataEditor.spec.js
new file mode 100644
index 0000000..91856ee
--- /dev/null
+++ b/web/cypress/integration/rawDataEditor/test-rawDataEditor.spec.js
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+/* eslint-disable no-undef */
+
+context('Test RawDataEditor', () => {
+ const timeout = 1000;
+
+ beforeEach(() => {
+ cy.login();
+
+ cy.fixture('rawDataEditor-dataset.json').as('dataset');
+ cy.fixture('selector.json').as('domSelector');
+ cy.fixture('data.json').as('data');
+ });
+
+ it('should create and update with rawDataEditor', function () {
+ const menuList = ['Route', 'Service', 'Upstream', 'Consumer'];
+ const publicData = this.data;
+ const dateset = this.dataset;
+ const domSelector = this.domSelector;
+ menuList.forEach(function (item) {
+ cy.visit('/');
+ cy.contains(item).click();
+ cy.contains('Create with Editor').click();
+ const data = dateset[item];
+
+ // create with editor
+ cy.window().then(({ codemirror }) => {
+ if (codemirror) {
+ codemirror.setValue(JSON.stringify(data));
+ }
+ cy.get(domSelector.drawer).should('exist');
+ cy.get(domSelector.drawer, { timeout }).within(() => {
+ cy.contains('Submit').click({
+ force: true,
+ });
+ cy.get(domSelector.drawer).should('not.exist');
+ });
+ });
+
+ cy.reload();
+ // update with editor
+ cy.contains(item === 'Consumer' ? data.username : data.name)
+ .siblings()
+ .contains('View')
+ .click();
+
+ cy.window().then(({ codemirror }) => {
+ if (codemirror) {
+ if (item === 'Consumer') {
+ codemirror.setValue(JSON.stringify({ ...data, desc: 'newDesc' }));
+ } else {
+ codemirror.setValue(JSON.stringify({ ...data, name: 'newName' }));
+ }
+ }
+ cy.get(domSelector.drawer).should('exist');
+ cy.get(domSelector.drawer, { timeout }).within(() => {
+ cy.contains('Submit').click({
+ force: true,
+ });
+ cy.get(domSelector.drawer).should('not.exist');
+ });
+ });
+
+ cy.reload();
+ cy.get(domSelector.tableBody).should('contain', item === 'Consumer' ? 'newDesc' : 'newName');
+
+ // delete resource
+ cy.contains(item === 'Consumer' ? 'newDesc' : 'newName')
+ .siblings()
+ .contains('Delete')
+ .click();
+ cy.contains('button', 'Confirm').click();
+
+ cy.get(domSelector.notification).should('contain', publicData[`delete${item}Success`]);
+ cy.get(domSelector.notificationClose).should('be.visible').click({
+ force: true,
+ multiple: true,
+ });
+ });
+ });
+});
diff --git a/web/src/components/RawDataEditor/RawDataEditor.tsx b/web/src/components/RawDataEditor/RawDataEditor.tsx
index ad8e209..588cebd 100644
--- a/web/src/components/RawDataEditor/RawDataEditor.tsx
+++ b/web/src/components/RawDataEditor/RawDataEditor.tsx
@@ -14,12 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React, { useRef } from 'react';
-import { Button, Drawer, PageHeader, Tooltip, notification } from 'antd';
+import React, { useEffect, useRef, useState } from 'react';
+import { Button, Drawer, PageHeader, notification, Space, Select } from 'antd';
import { LinkOutlined } from '@ant-design/icons';
import CodeMirror from '@uiw/react-codemirror';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { useIntl } from 'umi';
+import { js_beautify } from 'js-beautify';
+
+import { json2yaml, yaml2json } from '../../helpers';
type Props = {
visible: boolean,
@@ -27,11 +30,81 @@ type Props = {
type: 'route' | 'service' | 'consumer' | 'upstream'
data: Record<string, any>,
onClose?: () => void;
+ onSubmit?: (data: Record<string, any>) => void;
};
-const RawDataEditor: React.FC<Props> = ({ visible, readonly = true, type, data = {}, onClose }) => {
+enum codeMirrorModeList {
+ Json = 'Json',
+ Yaml = 'Yaml',
+}
+
+const RawDataEditor: React.FC<Props> = ({ visible, readonly = true, type, data = {}, onClose = () => { }, onSubmit = () => { } }) => {
const ref = useRef<any>(null);
const { formatMessage } = useIntl();
+ const [codeMirrorMode, setCodeMirrorMode] = useState<PluginComponent.CodeMirrorMode>(
+ codeMirrorModeList.Json,
+ );
+
+ useEffect(() => {
+ setCodeMirrorMode(codeMirrorModeList.Json);
+ }, [visible])
+
+ const modeOptions = [
+ { label: codeMirrorModeList.Json, value: codeMirrorModeList.Json },
+ { label: codeMirrorModeList.Yaml, value: codeMirrorModeList.Yaml },
+ ];
+
+ const handleModeChange = (value: PluginComponent.CodeMirrorMode) => {
+ switch (value) {
+ case codeMirrorModeList.Json: {
+ const { data: yamlData, error } = yaml2json(ref.current.editor.getValue(), true);
+
+ if (error) {
+ notification.error({
+ message: 'Invalid Yaml data',
+ });
+ return;
+ }
+ ref.current.editor.setValue(
+ js_beautify(yamlData, {
+ indent_size: 2,
+ }),
+ );
+ break;
+ }
+ case codeMirrorModeList.Yaml: {
+ const { data: jsonData, error } = json2yaml(ref.current.editor.getValue());
+
+ if (error) {
+ notification.error({
+ message: 'Invalid Json data',
+ });
+ return;
+ }
+ ref.current.editor.setValue(jsonData);
+ break;
+ }
+ default:
+ break;
+ }
+ setCodeMirrorMode(value);
+ };
+
+ const formatCodes = () => {
+ try {
+ if (ref.current) {
+ ref.current.editor.setValue(
+ js_beautify(ref.current.editor.getValue(), {
+ indent_size: 2,
+ }),
+ );
+ }
+ } catch (error) {
+ notification.error({
+ message: 'Format failed',
+ });
+ }
+ };
return (
<>
@@ -41,6 +114,34 @@ const RawDataEditor: React.FC<Props> = ({ visible, readonly = true, type, data =
width={700}
visible={visible}
onClose={onClose}
+ footer={
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <Button onClick={onClose} key={1}>
+ {formatMessage({ id: 'component.global.cancel' })}
+ </Button>
+ <Space>
+ <Button
+ key={2}
+ type="primary"
+ onClick={() => {
+ try {
+ const editorData =
+ codeMirrorMode === codeMirrorModeList.Json
+ ? JSON.parse(ref.current?.editor.getValue())
+ : yaml2json(ref.current?.editor.getValue(), false).data;
+ onSubmit(editorData);
+ } catch (error) {
+ notification.error({
+ message: 'Invalid JSON data',
+ });
+ }
+ }}
+ >
+ {formatMessage({ id: 'component.global.submit' })}
+ </Button>
+ </Space>
+ </div>
+ }
>
<PageHeader
title=""
@@ -57,11 +158,18 @@ const RawDataEditor: React.FC<Props> = ({ visible, readonly = true, type, data =
>
Document
</Button>,
- <Tooltip placement="top" title={formatMessage({ id: 'component.rawDataEditor.tip' })}>
- <Button type="primary" onClick={() => { }} key={2}>
- Format
- </Button>
- </Tooltip>,
+ <Select
+ defaultValue={codeMirrorModeList.Json}
+ value={codeMirrorMode}
+ options={modeOptions}
+ onChange={(value: PluginComponent.CodeMirrorMode) => {
+ handleModeChange(value);
+ }}
+ data-cy='code-mirror-mode'
+ ></Select>,
+ <Button type="primary" onClick={formatCodes} key={2}>
+ Format
+ </Button>,
<CopyToClipboard text={JSON.stringify(data)} onCopy={(_: string, result: boolean) => {
if (!result) {
notification.error({
diff --git a/web/src/constants.ts b/web/src/constants.ts
index 3df1c0c..f924a05 100644
--- a/web/src/constants.ts
+++ b/web/src/constants.ts
@@ -33,3 +33,5 @@ export const codeMessage = {
};
export const DEFAULT_GLOBAL_RULE_ID = '1';
+
+export const DELETE_FIELDS = ['id', 'create_time', 'update_time'];
diff --git a/web/src/locales/en-US/component.ts b/web/src/locales/en-US/component.ts
index 0579b26..ee763d0 100644
--- a/web/src/locales/en-US/component.ts
+++ b/web/src/locales/en-US/component.ts
@@ -25,6 +25,7 @@ export default {
'component.global.cancel': 'Cancel',
'component.global.submit': 'Submit',
'component.global.create': 'Create',
+ 'component.global.createWithEditor': 'Create with Editor',
'component.global.add': 'Add',
'component.global.save': 'Save',
'component.global.edit': 'Edit',
diff --git a/web/src/locales/zh-CN/component.ts b/web/src/locales/zh-CN/component.ts
index 1ee9e67..7076d05 100644
--- a/web/src/locales/zh-CN/component.ts
+++ b/web/src/locales/zh-CN/component.ts
@@ -25,6 +25,7 @@ export default {
'component.global.cancel': '取消',
'component.global.submit': '提交',
'component.global.create': '创建',
+ 'component.global.createWithEditor': '使用编辑器创建',
'component.global.add': '新建',
'component.global.save': '保存',
'component.global.edit': '编辑',
diff --git a/web/src/pages/Consumer/List.tsx b/web/src/pages/Consumer/List.tsx
index 0957383..713e377 100644
--- a/web/src/pages/Consumer/List.tsx
+++ b/web/src/pages/Consumer/List.tsx
@@ -22,17 +22,21 @@ import { Popconfirm, Button, notification } from 'antd';
import { history, useIntl } from 'umi';
import { PlusOutlined } from '@ant-design/icons';
import querystring from 'query-string'
+import { omit } from 'lodash';
+import { DELETE_FIELDS } from '@/constants';
import { timestampToLocaleString } from '@/helpers';
import { RawDataEditor } from '@/components/RawDataEditor';
-import { fetchList, remove } from './service';
+import { fetchList, remove, create, update } from './service';
const Page: React.FC = () => {
const ref = useRef<ActionType>();
const { formatMessage } = useIntl();
- const [rawDataEditorVisible, setRawDataEditorVisible] = useState(false);
- const [rawData, setRawData] = useState({});
+ const [visible, setVisible] = useState(false);
+ const [rawData, setRawData] = useState<Record<string, any>>({});
+ const [id, setId] = useState('');
+ const [editorMode, setEditorMode] = useState<'create' | 'update'>('create');
const [paginationConfig, setPaginationConfig] = useState({ pageSize: 10, current: 1 });
const savePageList = (page = 1, pageSize = 10) => {
@@ -74,8 +78,10 @@ const Page: React.FC = () => {
{formatMessage({ id: 'component.global.edit' })}
</Button>
<Button type="primary" style={{ marginRight: 10 }} onClick={() => {
- setRawData(record);
- setRawDataEditorVisible(true);
+ setId(record.id);
+ setRawData(omit(record, DELETE_FIELDS));
+ setVisible(true);
+ setEditorMode('update');
}}>
{formatMessage({ id: 'component.global.view' })}
</Button>
@@ -125,14 +131,29 @@ const Page: React.FC = () => {
<PlusOutlined />
{formatMessage({ id: 'component.global.create' })}
</Button>,
+ <Button type="primary" onClick={() => {
+ setVisible(true);
+ setEditorMode('create');
+ setRawData({});
+ }}>
+ <PlusOutlined />
+ {formatMessage({ id: 'component.global.createWithEditor' })}
+ </Button>,
]}
/>
<RawDataEditor
- visible={rawDataEditorVisible}
+ visible={visible}
type='consumer'
- readonly={true}
+ readonly={false}
data={rawData}
- onClose={() => { setRawDataEditorVisible(false) }}
+ onClose={() => { setVisible(false) }}
+ onSubmit={(data: any) => {
+ (editorMode === 'create' ? create(data) : update(id, data))
+ .then(() => {
+ setVisible(false);
+ ref.current?.reload();
+ })
+ }}
/>
</PageContainer>
);
diff --git a/web/src/pages/Route/List.tsx b/web/src/pages/Route/List.tsx
index c799bf2..2fbbe9f 100644
--- a/web/src/pages/Route/List.tsx
+++ b/web/src/pages/Route/List.tsx
@@ -38,10 +38,14 @@ import yaml from 'js-yaml';
import moment from 'moment';
import { saveAs } from 'file-saver';
import querystring from 'query-string'
+import { omit } from 'lodash';
+import { DELETE_FIELDS } from '@/constants';
import { timestampToLocaleString } from '@/helpers';
import type { RcFile } from 'antd/lib/upload';
import {
+ update,
+ create,
fetchList,
remove,
fetchLabelList,
@@ -74,8 +78,10 @@ const Page: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const [uploadFileList, setUploadFileList] = useState<RcFile[]>([]);
const [showImportModal, setShowImportModal] = useState(false);
- const [rawDataEditorVisible, setRawDataEditorVisible] = useState(false);
- const [rawData, setRawData] = useState({});
+ const [visible, setVisible] = useState(false);
+ const [rawData, setRawData] = useState<Record<string, any>>({});
+ const [id, setId] = useState('');
+ const [editorMode, setEditorMode] = useState<'create' | 'update'>('create');
const [paginationConfig, setPaginationConfig] = useState({ pageSize: 10, current: 1 });
const savePageList = (page = 1, pageSize = 10) => {
@@ -393,8 +399,10 @@ const Page: React.FC = () => {
{formatMessage({ id: 'component.global.edit' })}
</Button>
<Button type="primary" onClick={() => {
- setRawData(record);
- setRawDataEditorVisible(true);
+ setId(record.id);
+ setRawData(omit(record, DELETE_FIELDS));
+ setVisible(true);
+ setEditorMode('update');
}}>
{formatMessage({ id: 'component.global.view' })}
</Button>
@@ -450,6 +458,14 @@ const Page: React.FC = () => {
<PlusOutlined />
{formatMessage({ id: 'component.global.create' })}
</Button>,
+ <Button type="primary" onClick={() => {
+ setVisible(true);
+ setEditorMode('create');
+ setRawData({});
+ }}>
+ <PlusOutlined />
+ {formatMessage({ id: 'component.global.createWithEditor' })}
+ </Button>,
<Button
type="primary"
onClick={() => {
@@ -476,11 +492,18 @@ const Page: React.FC = () => {
}}
/>
<RawDataEditor
- visible={rawDataEditorVisible}
+ visible={visible}
type='route'
- readonly={true}
+ readonly={false}
data={rawData}
- onClose={() => { setRawDataEditorVisible(false) }}
+ onClose={() => { setVisible(false) }}
+ onSubmit={(data: any) => {
+ (editorMode === 'create' ? create(data, 'RawData') : update(id, data, 'RawData'))
+ .then(() => {
+ setVisible(false);
+ ref.current?.reload();
+ })
+ }}
/>
<Modal
title={formatMessage({ id: 'page.route.button.importOpenApi' })}
diff --git a/web/src/pages/Route/components/Step1/MatchingRulesView.tsx b/web/src/pages/Route/components/Step1/MatchingRulesView.tsx
index fdc4985..7216bee 100644
--- a/web/src/pages/Route/components/Step1/MatchingRulesView.tsx
+++ b/web/src/pages/Route/components/Step1/MatchingRulesView.tsx
@@ -170,6 +170,7 @@ const MatchingRulesView: React.FC<RouteModule.Step1PassProps> = ({
}}
okText={formatMessage({ id: 'component.global.confirm' })}
cancelText={formatMessage({ id: 'component.global.cancel' })}
+ maskClosable={false}
destroyOnClose
>
<Form form={modalForm} layout="vertical">
diff --git a/web/src/pages/Route/service.ts b/web/src/pages/Route/service.ts
index 1d3792e..f2edf2e 100644
--- a/web/src/pages/Route/service.ts
+++ b/web/src/pages/Route/service.ts
@@ -24,16 +24,16 @@ import {
} from './transform';
import { transformLabelList } from '@/helpers';
-export const create = (data: RouteModule.RequestData) =>
+export const create = (data: RouteModule.RequestData, mode?: RouteModule.RequestMode) =>
request(`/routes`, {
method: 'POST',
- data: transformStepData(data),
+ data: mode === 'RawData' ? data : transformStepData(data),
});
-export const update = (rid: number, data: RouteModule.RequestData) =>
+export const update = (rid: string, data: RouteModule.RequestData, mode?: RouteModule.RequestMode) =>
request(`/routes/${rid}`, {
method: 'PUT',
- data: transformStepData(data),
+ data: mode === 'RawData' ? data : transformStepData(data),
});
export const fetchItem = (rid: number) =>
diff --git a/web/src/pages/Route/typing.d.ts b/web/src/pages/Route/typing.d.ts
index 5145f3e..5fed522 100644
--- a/web/src/pages/Route/typing.d.ts
+++ b/web/src/pages/Route/typing.d.ts
@@ -21,6 +21,8 @@ declare namespace RouteModule {
type RequestProtocol = 'https' | 'http' | 'websocket';
+ type RequestMode = 'Normal' | 'RawData';
+
type BaseData = {
id?: number;
name: string;
@@ -145,7 +147,7 @@ declare namespace RouteModule {
regex_uri?: string[];
host?: string;
kvHeaders?: Kvobject[];
- headers?: Record<string,string>;
+ headers?: Record<string, string>;
}
type AdvancedMatchingRules = {
diff --git a/web/src/pages/Service/List.tsx b/web/src/pages/Service/List.tsx
index 467be78..39f7970 100644
--- a/web/src/pages/Service/List.tsx
+++ b/web/src/pages/Service/List.tsx
@@ -22,15 +22,19 @@ import type { ActionType, ProColumns } from '@ant-design/pro-table';
import { PlusOutlined } from '@ant-design/icons';
import { Button, notification, Popconfirm, Space } from 'antd';
import querystring from 'query-string'
+import { omit } from 'lodash';
+import { DELETE_FIELDS } from '@/constants';
import { RawDataEditor } from '@/components/RawDataEditor';
-import { fetchList, remove } from './service';
+import { fetchList, remove, create, update } from './service';
const Page: React.FC = () => {
const ref = useRef<ActionType>();
const { formatMessage } = useIntl();
- const [rawDataEditorVisible, setRawDataEditorVisible] = useState(false);
- const [rawData, setRawData] = useState({});
+ const [visible, setVisible] = useState(false);
+ const [rawData, setRawData] = useState<Record<string, any>>({});
+ const [id, setId] = useState('');
+ const [editorMode, setEditorMode] = useState<'create' | 'update'>('create');
const [paginationConfig, setPaginationConfig] = useState({ pageSize: 10, current: 1 });
const savePageList = (page = 1, pageSize = 10) => {
@@ -71,8 +75,10 @@ const Page: React.FC = () => {
{formatMessage({ id: 'component.global.edit' })}
</Button>
<Button type="primary" onClick={() => {
- setRawData(record);
- setRawDataEditorVisible(true);
+ setId(record.id);
+ setRawData(omit(record, DELETE_FIELDS));
+ setVisible(true);
+ setEditorMode('update');
}}>
{formatMessage({ id: 'component.global.view' })}
</Button>
@@ -123,14 +129,29 @@ const Page: React.FC = () => {
<PlusOutlined />
{formatMessage({ id: 'component.global.create' })}
</Button>,
+ <Button type="primary" onClick={() => {
+ setVisible(true);
+ setEditorMode('create');
+ setRawData({});
+ }}>
+ <PlusOutlined />
+ {formatMessage({ id: 'component.global.createWithEditor' })}
+ </Button>,
]}
/>
<RawDataEditor
- visible={rawDataEditorVisible}
+ visible={visible}
type='service'
- readonly={true}
+ readonly={false}
data={rawData}
- onClose={() => { setRawDataEditorVisible(false) }}
+ onClose={() => { setVisible(false) }}
+ onSubmit={(data: any) => {
+ (editorMode === 'create' ? create(data) : update(id, data))
+ .then(() => {
+ setVisible(false);
+ ref.current?.reload();
+ })
+ }}
/>
</PageHeaderWrapper>
);
diff --git a/web/src/pages/Upstream/List.tsx b/web/src/pages/Upstream/List.tsx
index ce5d6e7..4872955 100644
--- a/web/src/pages/Upstream/List.tsx
+++ b/web/src/pages/Upstream/List.tsx
@@ -22,20 +22,26 @@ import { Popconfirm, Button, notification } from 'antd';
import { history, useIntl } from 'umi';
import { PlusOutlined } from '@ant-design/icons';
import querystring from 'query-string'
+import { omit } from 'lodash';
+
+import { DELETE_FIELDS } from '@/constants';
import { RawDataEditor } from '@/components/RawDataEditor';
import { timestampToLocaleString } from '@/helpers';
-import { fetchList, remove } from './service';
+import { fetchList, remove, create, update } from './service';
const Page: React.FC = () => {
const ref = useRef<ActionType>();
- const [rawDataEditorVisible, setRawDataEditorVisible] = useState(false);
- const [rawData, setRawData] = useState({});
+ const [visible, setVisible] = useState(false);
+ const [rawData, setRawData] = useState<Record<string, any>>({});
+ const [id, setId] = useState('');
+ const [editorMode, setEditorMode] = useState<'create' | 'update'>('create');
const [paginationConfig, setPaginationConfig] = useState({ pageSize: 10, current: 1 });
const { formatMessage } = useIntl();
+
const savePageList = (page = 1, pageSize = 10) => {
history.replace(`/upstream/list?page=${page}&pageSize=${pageSize}`);
};
@@ -80,8 +86,10 @@ const Page: React.FC = () => {
{formatMessage({ id: 'page.upstream.list.edit' })}
</Button>
<Button type="primary" onClick={() => {
- setRawData(record);
- setRawDataEditorVisible(true);
+ setId(record.id);
+ setRawData(omit(record, DELETE_FIELDS));
+ setVisible(true);
+ setEditorMode('update');
}}>
{formatMessage({ id: 'component.global.view' })}
</Button>
@@ -129,14 +137,29 @@ const Page: React.FC = () => {
<PlusOutlined />
{formatMessage({ id: 'component.global.create' })}
</Button>,
+ <Button type="primary" onClick={() => {
+ setVisible(true);
+ setEditorMode('create');
+ setRawData({});
+ }}>
+ <PlusOutlined />
+ {formatMessage({ id: 'component.global.createWithEditor' })}
+ </Button>,
]}
/>
<RawDataEditor
- visible={rawDataEditorVisible}
+ visible={visible}
type='upstream'
- readonly={true}
+ readonly={false}
data={rawData}
- onClose={() => { setRawDataEditorVisible(false) }}
+ onClose={() => { setVisible(false) }}
+ onSubmit={(data: any) => {
+ (editorMode === 'create' ? create(data) : update(id, data))
+ .then(() => {
+ setVisible(false);
+ ref.current?.reload();
+ })
+ }}
/>
</PageContainer>
);