You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2021/05/11 06:36:24 UTC
[apisix-dashboard] branch master updated: feat: refactor Plugin
Orchestration (#1813)
This is an automated email from the ASF dual-hosted git repository.
juzhiyuan 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 423c9e8 feat: refactor Plugin Orchestration (#1813)
423c9e8 is described below
commit 423c9e899681af166ee2e83038ddb07d62a2f38c
Author: 琚致远 <ju...@apache.org>
AuthorDate: Tue May 11 14:36:16 2021 +0800
feat: refactor Plugin Orchestration (#1813)
---
.actions/ASF-Release.cfg | 5 +-
.github/workflows/frontend-plugin-e2e-test.yml | 4 +
LICENSE | 1 +
licenses/LICENSE-antvis-x6.txt | 21 ++
.../create-route-with-plugin-orchestration.spec.js | 132 +++++++
web/cypress/support/commands.js | 1 +
web/package.json | 4 +-
web/src/components/Plugin/PluginDetail.tsx | 299 ++++++++--------
web/src/components/Plugin/service.ts | 14 +
web/src/components/PluginFlow/PluginFlow.tsx | 203 +++++++++++
.../PluginFlow/components/FlowGraph/FlowGraph.ts | 394 +++++++++++++++++++++
.../components/FlowGraph}/index.ts | 3 +-
.../PluginFlow/components/FlowGraph/shapes.ts | 161 +++++++++
.../PluginFlow/components/Toolbar/index.tsx | 195 ++++++++++
web/src/components/PluginFlow/constants.ts | 294 +++++++++++++++
.../components/Page.tsx => PluginFlow/index.ts} | 5 +-
web/src/components/PluginFlow/locales/en-US.ts | 41 +++
web/src/components/PluginFlow/locales/zh-CN.ts | 41 +++
.../typing.d.ts => PluginFlow/style.less} | 34 +-
.../PluginOrchestration/DrawPluginStyle.ts | 82 -----
.../PluginOrchestration/components/SidebarItem.tsx | 42 ---
.../components/PluginOrchestration/constants.ts | 62 ----
.../PluginOrchestration/customConfig.tsx | 77 ----
web/src/components/PluginOrchestration/index.tsx | 289 ---------------
.../PluginOrchestration/locales/en-US.ts | 30 --
.../PluginOrchestration/locales/zh-CN.ts | 30 --
web/src/components/PluginOrchestration/service.ts | 25 --
.../components/PluginOrchestration/transform.ts | 122 -------
web/src/locales/en-US.ts | 4 +-
web/src/locales/zh-CN.ts | 4 +-
web/src/locales/zh-CN/component.ts | 2 +-
web/src/pages/Route/Create.tsx | 61 +++-
.../Route/components/CreateStep4/CreateStep4.tsx | 10 +-
.../Route/components/DebugViews/DebugDrawView.tsx | 24 +-
web/src/pages/Route/components/Step3/index.tsx | 42 ++-
web/src/pages/Route/constants.ts | 9 -
web/src/pages/Route/locales/en-US.ts | 4 +-
web/src/pages/Route/locales/zh-CN.ts | 2 +-
web/src/pages/Service/locales/en-US.ts | 4 +-
web/yarn.lock | 167 ++++++---
40 files changed, 1901 insertions(+), 1043 deletions(-)
diff --git a/.actions/ASF-Release.cfg b/.actions/ASF-Release.cfg
index 341e221..9bf3581 100644
--- a/.actions/ASF-Release.cfg
+++ b/.actions/ASF-Release.cfg
@@ -69,11 +69,12 @@ ASFLicenseHeaderLua.txt
# Skip files containing MIT License
web/scripts/verifyCommit.js
-web/src/components/HeaderDropdown/index.less
-web/src/components/HeaderDropdown/index.tsx
+web/src/components/HeaderDropdown
web/src/components/NoticeIcon
web/src/components/PageLoading/index.tsx
web/src/components/RightContent
+web/src/components/PluginFlow/components/ConfigPanel
+web/src/components/PluginFlow/components/Toolbar
web/src/e2e/__mocks__/antd-pro-merge-less.js
web/src/e2e/baseLayout.e2e.js
web/src/pages/404.tsx
diff --git a/.github/workflows/frontend-plugin-e2e-test.yml b/.github/workflows/frontend-plugin-e2e-test.yml
index d1d7e8f..1ce91d6 100644
--- a/.github/workflows/frontend-plugin-e2e-test.yml
+++ b/.github/workflows/frontend-plugin-e2e-test.yml
@@ -44,6 +44,10 @@ jobs:
with:
go-version: '1.13'
+ - name: Download dag-to-lua
+ working-directory: ./
+ run: make dag-lib
+
- name: Start manager-api
working-directory: ./api
run: |
diff --git a/LICENSE b/LICENSE
index 112687a..36ca18c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -216,6 +216,7 @@ The following components are provided under the MIT License. See project link fo
The text of each license is also included at licenses/LICENSE-[project].txt.
files from ant-design-pro: https://github.com/ant-design/ant-design-pro MIT
+ files from antvis-x6: https://github.com/antvis/X6 MIT
files from json.lua: https://github.com/rxi/json.lua MIT
========================================================================
diff --git a/licenses/LICENSE-antvis-x6.txt b/licenses/LICENSE-antvis-x6.txt
new file mode 100644
index 0000000..c17ba2f
--- /dev/null
+++ b/licenses/LICENSE-antvis-x6.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Alipay.inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/web/cypress/integration/plugin/create-route-with-plugin-orchestration.spec.js b/web/cypress/integration/plugin/create-route-with-plugin-orchestration.spec.js
new file mode 100644
index 0000000..867baad
--- /dev/null
+++ b/web/cypress/integration/plugin/create-route-with-plugin-orchestration.spec.js
@@ -0,0 +1,132 @@
+/*
+ * 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('Create and delete route with plugin orchestration', () => {
+ const selector = {
+ empty: '.ant-empty-normal',
+ name: '#name',
+ description: '#desc',
+ nodes_0_host: '#nodes_0_host',
+ nodes_0_port: '#nodes_0_port',
+ nodes_0_weight: '#nodes_0_weight',
+ groupButton: '.ant-radio-group',
+ canvas: '.x6-graph-svg',
+ startNode:
+ '#stencil > div > div.x6-widget-stencil-content > div:nth-child(1) > div > div > svg > g > g.x6-graph-svg-stage > g:nth-child(1) > g > circle',
+ notification: '.ant-notification-notice-message',
+ notificationClose: '.anticon-close',
+ nodeInput: '.x6-widget-stencil input[type="search"]',
+ hiddenGroup: '.x6-widget-stencil-group.unmatched',
+ canvasNode: '#container > svg > g > g.x6-graph-svg-stage > g:nth-child(2) > rect',
+ canvasContainer: '#container',
+ drawer: '.ant-drawer-content',
+ deleteAlert: '.ant-modal-body',
+ codemirrorScroll: '.CodeMirror-scroll',
+ };
+
+ beforeEach(() => {
+ cy.login();
+ });
+
+ it('should create route with plugin orchestration', function () {
+ cy.visit('/');
+ cy.contains('Route').click();
+ cy.get(selector.empty).should('be.visible');
+ cy.contains('Create').click();
+ cy.contains('Next').click().click();
+ cy.get(selector.name).type('routeName');
+ cy.get(selector.description).type('desc');
+ cy.contains('Next').click();
+
+ cy.get(selector.nodes_0_host).type('127.0.0.1');
+ cy.get(selector.nodes_0_port).clear().type('80');
+ cy.get(selector.nodes_0_weight).clear().type('1');
+ cy.contains('Next').click();
+
+ cy.get(selector.groupButton).contains('Orchestration').click();
+ cy.get(selector.canvas).should('be.visible');
+
+ // Plugin Orchestration
+ cy.get(selector.startNode).move({ x: 400, y: 0, force: true, position: 'center' });
+ cy.contains('Next').click();
+ cy.get(selector.notification).should('contain', 'Root node not found');
+ cy.get(selector.notificationClose).click().should('not.be.visible');
+
+ cy.get(selector.nodeInput).type('key-auth');
+ cy.get(selector.hiddenGroup).should('not.be.visible');
+ cy.contains('key-auth').move({ x: 300, y: 0, force: true, position: 'center' });
+ cy.contains('Next').click();
+ cy.get(selector.notification).should('contain', 'Root node not found');
+ cy.get(selector.notificationClose).click().should('not.be.visible');
+
+ // Linking nodes
+ cy.get(selector.canvasNode)
+ .click()
+ .then(() => {
+ const node2 = cy
+ .get('#container > svg > g > g.x6-graph-svg-stage > g:nth-child(2) > g > circle')
+ .eq(0);
+ const node1 = cy
+ .get('#container > svg > g > g.x6-graph-svg-stage > g:nth-child(1) > g > circle')
+ .eq(0);
+ node1
+ .trigger('mousedown')
+ .trigger('mousemove', { x: 0, y: 150, force: true })
+ .trigger('mouseup', { force: true });
+ });
+
+ cy.contains('Next').click();
+ cy.get(selector.notification).should('contain', 'Found node without configuration');
+ cy.get(selector.notificationClose).click().should('not.be.visible');
+
+ // Configuration plugins and submit
+ cy.get(selector.canvasContainer)
+ .click()
+ .within(() => {
+ cy.contains('key-auth').dblclick();
+ });
+ cy.contains('Submit').click();
+ cy.get(selector.drawer).should('not.exist');
+ cy.contains('Next').click();
+ cy.contains('Submit').click();
+
+ cy.contains('Submit Successfully');
+ cy.contains('Goto List').click();
+ cy.url().should('contains', 'routes/list');
+ });
+
+ it('should view and delete the route', function () {
+ cy.visit('/routes/list');
+ cy.contains('routeName').siblings().contains('More').click();
+ cy.contains('View').click();
+ cy.get(selector.codemirrorScroll).within(() => {
+ cy.contains('script').should('exist');
+ });
+ cy.contains('Cancel').click();
+
+ // Delete the route
+ cy.contains('routeName').siblings().contains('More').click();
+ cy.contains('Delete').click();
+ cy.get(selector.deleteAlert)
+ .should('be.visible')
+ .within(() => {
+ cy.contains('OK').click();
+ });
+ cy.get(selector.notification).should('contain', 'Delete Route Successfully');
+ });
+});
diff --git a/web/cypress/support/commands.js b/web/cypress/support/commands.js
index 4592efb..9a8a1f9 100644
--- a/web/cypress/support/commands.js
+++ b/web/cypress/support/commands.js
@@ -17,6 +17,7 @@
/* eslint-disable no-undef */
import defaultSettings from '../../config/defaultSettings';
import 'cypress-file-upload';
+import '@4tw/cypress-drag-drop';
Cypress.Commands.add('login', () => {
const { SERVE_ENV = 'dev' } = Cypress.env();
diff --git a/web/package.json b/web/package.json
index 23337e4..8f425a4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -52,7 +52,8 @@
"@ant-design/icons": "^4.0.0",
"@ant-design/pro-layout": "^6.0.0",
"@ant-design/pro-table": "2.30.1",
- "@mrblenny/react-flow-chart": "^0.0.14",
+ "@antv/x6": "^1.18.5",
+ "@antv/x6-react-components": "^1.1.7",
"@rjsf/antd": "2.2.0",
"@rjsf/core": "2.2.0",
"@types/js-yaml": "^4.0.0",
@@ -88,6 +89,7 @@
"yaml": "^1.10.0"
},
"devDependencies": {
+ "@4tw/cypress-drag-drop": "^1.6.0",
"@ant-design/pro-cli": "^2.0.2",
"@cypress/code-coverage": "^3.9.2",
"@types/base-64": "^0.1.3",
diff --git a/web/src/components/Plugin/PluginDetail.tsx b/web/src/components/Plugin/PluginDetail.tsx
index addaf3c..0028116 100644
--- a/web/src/components/Plugin/PluginDetail.tsx
+++ b/web/src/components/Plugin/PluginDetail.tsx
@@ -53,6 +53,7 @@ type Props = {
readonly?: boolean;
visible: boolean;
maskClosable?: boolean;
+ isEnabled?: boolean;
onClose?: () => void;
onChange?: (data: any) => void;
};
@@ -91,6 +92,7 @@ const PluginDetail: React.FC<Props> = ({
pluginList = [],
readonly = false,
maskClosable = true,
+ isEnabled = false,
initialData = {},
onClose = () => { },
onChange = () => { },
@@ -144,7 +146,7 @@ const PluginDetail: React.FC<Props> = ({
useEffect(() => {
form.setFieldsValue({
- disable: initialData[name] && !initialData[name].disable,
+ disable: isEnabled ? true : (initialData[name] && !initialData[name].disable),
scope: 'global',
});
if (PLUGIN_UI_LIST.includes(name)) {
@@ -272,75 +274,77 @@ const PluginDetail: React.FC<Props> = ({
}
};
- return (
- <>
- <Drawer
- title={formatMessage({ id: 'component.plugin.editor' })}
- visible={visible}
- placement="right"
- closable={false}
- maskClosable={maskClosable}
- onClose={onClose}
- width={700}
- footer={
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
- {' '}
- <Button onClick={onClose} key={1}>
- {formatMessage({ id: 'component.global.cancel' })}
- </Button>
- <Space>
- <Popconfirm
- title={formatMessage({ id: 'page.plugin.drawer.popconfirm.title.delete' })}
- okText={formatMessage({ id: 'component.global.confirm' })}
- cancelText={formatMessage({ id: 'component.global.cancel' })}
- disabled={readonly}
- onConfirm={() => {
- onChange({
- formData: form.getFieldsValue(),
- codemirrorData: {},
- shouldDelete: true,
- });
- }}
- >
- {initialData[name] ? (
- <Button key={3} type="primary" danger disabled={readonly}>
- {formatMessage({ id: 'component.global.delete' })}
- </Button>
- ) : null}
- </Popconfirm>
- <Button
- key={2}
- disabled={readonly}
- type="primary"
- onClick={() => {
- try {
- let editorData;
- if (codeMirrorMode === codeMirrorModeList.JSON) {
- editorData = JSON.parse(ref.current?.editor.getValue());
- } else if (codeMirrorMode === codeMirrorModeList.YAML) {
- editorData = yaml2json(ref.current?.editor.getValue(), false).data;
- } else {
- editorData = getUIFormData();
- }
+ const isNoConfigurationRequired = pluginType === PluginType.authentication && schemaType !== 'consumer' && (codeMirrorMode !== codeMirrorModeList.UIForm)
- validateData(name, editorData).then((value) => {
- onChange({ formData: form.getFieldsValue(), codemirrorData: value });
- });
- } catch (error) {
- notification.error({
- message: 'Invalid JSON data',
- });
+ return (
+ <Drawer
+ title={formatMessage({ id: 'component.plugin.editor' })}
+ visible={visible}
+ placement="right"
+ closable={false}
+ maskClosable={maskClosable}
+ destroyOnClose
+ onClose={onClose}
+ width={700}
+ footer={
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+ {' '}
+ <Button onClick={onClose} key={1}>
+ {formatMessage({ id: 'component.global.cancel' })}
+ </Button>
+ <Space>
+ <Popconfirm
+ title={formatMessage({ id: 'page.plugin.drawer.popconfirm.title.delete' })}
+ okText={formatMessage({ id: 'component.global.confirm' })}
+ cancelText={formatMessage({ id: 'component.global.cancel' })}
+ disabled={readonly}
+ onConfirm={() => {
+ onChange({
+ formData: form.getFieldsValue(),
+ codemirrorData: {},
+ shouldDelete: true,
+ });
+ }}
+ >
+ {initialData[name] ? (
+ <Button key={3} type="primary" danger disabled={readonly}>
+ {formatMessage({ id: 'component.global.delete' })}
+ </Button>
+ ) : null}
+ </Popconfirm>
+ <Button
+ key={2}
+ disabled={readonly}
+ type="primary"
+ onClick={() => {
+ try {
+ let editorData;
+ if (codeMirrorMode === codeMirrorModeList.JSON) {
+ editorData = JSON.parse(ref.current?.editor.getValue());
+ } else if (codeMirrorMode === codeMirrorModeList.YAML) {
+ editorData = yaml2json(ref.current?.editor.getValue(), false).data;
+ } else {
+ editorData = getUIFormData();
}
- }}
- >
- {formatMessage({ id: 'component.global.submit' })}
- </Button>
- </Space>
- </div>
- }
- >
- <style>
- {`
+
+ validateData(name, editorData).then((value) => {
+ onChange({ formData: form.getFieldsValue(), codemirrorData: value });
+ });
+ } catch (error) {
+ notification.error({
+ message: 'Invalid JSON data',
+ });
+ }
+ }}
+ >
+ {formatMessage({ id: 'component.global.submit' })}
+ </Button>
+ </Space>
+ </div>
+ }
+ >
+ <style>
+ {`
.site-page-header {
border: 1px solid rgb(235, 237, 240);
margin-top:10px;
@@ -349,89 +353,88 @@ const PluginDetail: React.FC<Props> = ({
color: #000;
}
`}
- </style>
+ </style>
- <Form {...FORM_ITEM_LAYOUT} style={{ marginTop: '10px' }} form={form}>
- <Form.Item label={formatMessage({ id: 'component.global.name' })}>
- <Input value={name} bordered={false} disabled />
+ <Form {...FORM_ITEM_LAYOUT} style={{ marginTop: '10px' }} form={form}>
+ <Form.Item label={formatMessage({ id: 'component.global.name' })}>
+ <Input value={name} bordered={false} disabled />
+ </Form.Item>
+ <Form.Item label={formatMessage({ id: 'component.global.enable' })} valuePropName="checked" name="disable">
+ <Switch
+ defaultChecked={isEnabled ? true : initialData[name] && !initialData[name].disable}
+ disabled={readonly || isEnabled}
+ />
+ </Form.Item>
+ {type === 'global' && (
+ <Form.Item label={formatMessage({ id: 'component.global.scope' })} name="scope">
+ <Select disabled>
+ <Select.Option value="global">{formatMessage({ id: "other.global" })}</Select.Option>
+ </Select>
</Form.Item>
- <Form.Item label={formatMessage({ id: 'component.global.enable' })} valuePropName="checked" name="disable">
- <Switch
- defaultChecked={initialData[name] && !initialData[name].disable}
- disabled={readonly}
- />
- </Form.Item>
- {type === 'global' && (
- <Form.Item label={formatMessage({ id: 'component.global.scope' })} name="scope">
- <Select disabled>
- <Select.Option value="global">{formatMessage({ id: "other.global" })}</Select.Option>
- </Select>
- </Form.Item>
- )}
- </Form>
- <Divider orientation="left">{formatMessage({ id: 'component.global.data.editor' })}</Divider>
- <PageHeader
- title=""
- subTitle={
- pluginType === PluginType.authentication && schemaType !== 'consumer' && (codeMirrorMode !== codeMirrorModeList.UIForm) ? (
- <Alert message={formatMessage({ id: 'component.plugin.noConfigurationRequired' })} type="warning" />
- ) : null
- }
- ghost={false}
- extra={[
- <Select
- defaultValue={codeMirrorModeList.JSON}
- value={codeMirrorMode}
- options={modeOptions}
- onChange={(value: PluginComponent.CodeMirrorMode) => {
- handleModeChange(value);
- }}
- data-cy='code-mirror-mode'
- key={1}
- ></Select>,
- <Tooltip title={formatMessage({ id: "component.plugin.format-codes.disable" })} key={2}>
- <Button type="primary" onClick={formatCodes} disabled={codeMirrorMode === codeMirrorModeList.UIForm}>
- {formatMessage({ id: 'component.global.format' })}
- </Button>
- </Tooltip>,
- <Button
- type="default"
- icon={<LinkOutlined />}
- onClick={() => {
- if (name.startsWith('serverless')) {
- window.open('https://apisix.apache.org/docs/apisix/plugins/serverless');
- } else {
- window.open(`https://apisix.apache.org/docs/apisix/plugins/${name}`);
- }
- }}
- key={3}
- >
- {formatMessage({ id: 'component.global.document' })}
+ )}
+ </Form>
+ <Divider orientation="left">{formatMessage({ id: 'component.global.data.editor' })}</Divider>
+ <PageHeader
+ title=""
+ subTitle={
+ isNoConfigurationRequired ? (
+ <Alert message={formatMessage({ id: 'component.plugin.noConfigurationRequired' })} type="warning" />
+ ) : null
+ }
+ ghost={false}
+ extra={[
+ <Select
+ defaultValue={codeMirrorModeList.JSON}
+ value={codeMirrorMode}
+ options={modeOptions}
+ onChange={(value: PluginComponent.CodeMirrorMode) => {
+ handleModeChange(value);
+ }}
+ data-cy='code-mirror-mode'
+ key={1}
+ ></Select>,
+ <Tooltip title={formatMessage({ id: "component.plugin.format-codes.disable" })} key={2}>
+ <Button type="primary" onClick={formatCodes} disabled={codeMirrorMode === codeMirrorModeList.UIForm}>
+ {formatMessage({ id: 'component.global.format' })}
</Button>
- ]}
- />
- {Boolean(codeMirrorMode === codeMirrorModeList.UIForm) && <PluginForm name={name} form={UIForm} renderForm={!(pluginType === PluginType.authentication && schemaType !== 'consumer')} />}
- <div style={{ display: codeMirrorMode === codeMirrorModeList.UIForm ? 'none' : 'unset' }}><CodeMirror
- ref={(codemirror) => {
- ref.current = codemirror;
- if (codemirror) {
- // NOTE: for debug & test
- // @ts-ignore
- window.codemirror = codemirror.editor;
- }
- }}
- value={JSON.stringify(data, null, 2)}
- options={{
- mode: codeMirrorMode,
- readOnly: readonly ? 'nocursor' : '',
- lineWrapping: true,
- lineNumbers: true,
- showCursorWhenSelecting: true,
- autofocus: true,
- }} />
- </div>
- </Drawer>
- </>
+ </Tooltip>,
+ <Button
+ type="default"
+ icon={<LinkOutlined />}
+ onClick={() => {
+ if (name.startsWith('serverless')) {
+ window.open('https://apisix.apache.org/docs/apisix/plugins/serverless');
+ } else {
+ window.open(`https://apisix.apache.org/docs/apisix/plugins/${name}`);
+ }
+ }}
+ key={3}
+ >
+ {formatMessage({ id: 'component.global.document' })}
+ </Button>
+ ]}
+ />
+ {Boolean(codeMirrorMode === codeMirrorModeList.UIForm) && <PluginForm name={name} form={UIForm} renderForm={!(pluginType === PluginType.authentication && schemaType !== 'consumer')} />}
+ <div style={{ display: codeMirrorMode === codeMirrorModeList.UIForm ? 'none' : 'unset' }}><CodeMirror
+ ref={(codemirror) => {
+ ref.current = codemirror;
+ if (codemirror) {
+ // NOTE: for debug & test
+ // @ts-ignore
+ window.codemirror = codemirror.editor;
+ }
+ }}
+ value={JSON.stringify(data, null, 2)}
+ options={{
+ mode: codeMirrorMode,
+ readOnly: (readonly || isNoConfigurationRequired) ? 'nocursor' : '',
+ lineWrapping: true,
+ lineNumbers: true,
+ showCursorWhenSelecting: true,
+ autofocus: true,
+ }} />
+ </div>
+ </Drawer>
);
};
diff --git a/web/src/components/Plugin/service.ts b/web/src/components/Plugin/service.ts
index 89c7fee..98f04fc 100644
--- a/web/src/components/Plugin/service.ts
+++ b/web/src/components/Plugin/service.ts
@@ -19,7 +19,17 @@ import { request } from 'umi';
import { PLUGIN_LIST, PluginType } from './data';
+const cached: {
+ list: PluginComponent.Meta[]
+} = {
+ list: []
+}
+
export const fetchList = () => {
+ if (cached.list.length) {
+ return Promise.resolve(cached.list)
+ }
+
return request<Res<PluginComponent.Meta[]>>('/plugins?all=true').then((data) => {
const typedData = data.data.map(item => ({
...item,
@@ -33,6 +43,10 @@ export const fetchList = () => {
finalList = finalList.concat(typedData.filter(item => item.type === type))
})
+ if (cached.list.length === 0) {
+ cached.list = finalList
+ }
+
return finalList
});
};
diff --git a/web/src/components/PluginFlow/PluginFlow.tsx b/web/src/components/PluginFlow/PluginFlow.tsx
new file mode 100644
index 0000000..f49130c
--- /dev/null
+++ b/web/src/components/PluginFlow/PluginFlow.tsx
@@ -0,0 +1,203 @@
+/*
+ * 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 { Modal, Form, Input, Alert } from 'antd'
+import { Cell } from '@antv/x6'
+import { useIntl } from 'umi'
+
+import FlowGraph from './components/FlowGraph'
+import Toolbar from './components/Toolbar'
+import { DEFAULT_CONDITION_PROPS, DEFAULT_PLUGIN_PROPS, DEFAULT_STENCIL_WIDTH, DEFAULT_TOOLBAR_HEIGHT, FlowGraphEvent } from './constants'
+import styles from './style.less'
+import PluginDetail from '../Plugin/PluginDetail'
+import { fetchList } from '../Plugin/service'
+
+type Props = {
+ chart: {
+ cells: Cell.Properties[];
+ };
+ readonly?: boolean;
+}
+
+type PluginProps = {
+ id: string;
+ name: string;
+ visible: boolean;
+ data: any;
+}
+
+type ConditionProps = {
+ id: string;
+ visible: boolean;
+ data: string;
+}
+
+const PluginFlow: React.FC<Props> = ({ chart, readonly = false }) => {
+ const { formatMessage } = useIntl()
+
+ // NOTE: To prevent from graph is not initialized
+ const [isReady, setIsReady] = useState(false)
+ const [plugins, setPlugins] = useState<PluginComponent.Meta[]>([])
+
+ const [pluginProps, setPluginProps] = useState<PluginProps>(DEFAULT_PLUGIN_PROPS)
+ const [conditionProps, setConditionProps] = useState<ConditionProps>(DEFAULT_CONDITION_PROPS)
+
+ const getContainerSize = () => {
+ const leftSidebar = document.querySelector('aside.ant-layout-sider')
+ const blankSpaceWidth = 24 * 4
+
+ const globalHeaderHeight = 48
+ const pageHeaderHeight = 72
+ const otherHeight = 191
+
+ const width = document.body.offsetWidth - (leftSidebar?.clientWidth || 0) - blankSpaceWidth - DEFAULT_STENCIL_WIDTH
+ const height = document.body.offsetHeight - globalHeaderHeight - pageHeaderHeight - otherHeight
+
+ return {
+ width,
+ height: height < 800 ? 800 : height
+ }
+ }
+
+ useEffect(() => {
+ if (!plugins.length) {
+ return
+ }
+
+ const container = document.getElementById("container")
+ if (!container) {
+ return
+ }
+
+ const siderbarCollapsedButton = document.querySelector('.ant-pro-sider-collapsed-button')
+
+ const graph = FlowGraph.init(container, plugins, chart);
+ (window as any).graph = FlowGraph
+ setIsReady(true)
+
+ const stencilContainer = document.querySelector('#stencil') as HTMLElement
+
+ const handleResize = () => {
+ const { width, height } = getContainerSize()
+ graph.resize(width, height)
+
+ stencilContainer.style.height = `${height + DEFAULT_TOOLBAR_HEIGHT}px`
+ stencilContainer.style.width = `${DEFAULT_STENCIL_WIDTH}px`
+ }
+
+ const handleLeftSidebarResize = () => {
+ setTimeout(() => {
+ handleResize()
+ }, 200)
+ }
+
+ handleResize()
+
+ graph.on(FlowGraphEvent.PLUGIN_CHANGE, setPluginProps)
+ graph.on(FlowGraphEvent.CONDITION_CHANGE, (props: ConditionProps) => {
+ setConditionProps(props)
+ })
+
+ if (readonly) {
+ graph.disableKeyboard()
+ }
+
+ window.addEventListener("resize", handleResize)
+ siderbarCollapsedButton?.addEventListener('click', handleLeftSidebarResize)
+ // eslint-disable-next-line
+ return () => {
+ window.removeEventListener("resize", handleResize)
+ siderbarCollapsedButton?.removeEventListener('click', handleLeftSidebarResize)
+ }
+ }, [plugins])
+
+ useEffect(() => {
+ fetchList().then(setPlugins)
+ }, [])
+
+ return (
+ <React.Fragment>
+ {readonly && <Alert type="warning" message={formatMessage({ id: 'component.plugin-flow.text.preview.readonly' })} showIcon style={{ marginBottom: 20 }} />}
+ <div className={styles.container}>
+ <div id="stencil" className={styles.stencil} style={readonly ? { width: 0, height: 0 } : {}} />
+ <div className={styles.panel}>
+ <div className={styles.toolbar}>{isReady && <Toolbar />}</div>
+ <div id="container" className={styles.flow}></div>
+ </div>
+ </div>
+ {
+ pluginProps.visible && (
+ <PluginDetail
+ readonly={readonly}
+ schemaType="route"
+ name={pluginProps.name}
+ visible={pluginProps.visible}
+ pluginList={plugins}
+ isEnabled
+ initialData={{
+ // NOTE: We use {PluginName: data} because initialData is all plugins' data
+ [pluginProps.name]: pluginProps.data
+ }}
+ onClose={() => {
+ setPluginProps(DEFAULT_PLUGIN_PROPS)
+ }}
+ onChange={({ formData, codemirrorData, shouldDelete }) => {
+ if (shouldDelete) {
+ FlowGraph.graph.removeCell(pluginProps.id)
+ } else {
+ const disable = !formData.disable
+ FlowGraph.setData(pluginProps.id, { ...codemirrorData, disable })
+ }
+ setPluginProps(DEFAULT_PLUGIN_PROPS)
+ }}
+ />
+ )
+ }
+
+ <Modal
+ visible={conditionProps.visible}
+ title={formatMessage({ id: 'component.plugin-flow.text.condition.required' })}
+ onOk={() => {
+ FlowGraph.setData(conditionProps.id, conditionProps.data);
+ setConditionProps(DEFAULT_CONDITION_PROPS)
+ }}
+ onCancel={() => setConditionProps(DEFAULT_CONDITION_PROPS)}
+ okText={formatMessage({ id: 'component.global.confirm' })}
+ cancelText={formatMessage({ id: 'component.global.cancel' })}
+ okButtonProps={{
+ disabled: readonly
+ }}
+ >
+ <Form.Item label={formatMessage({ id: 'component.plugin-flow.text.condition' })} style={{ marginBottom: 0 }} tooltip={formatMessage({ id: 'component.plugin-flow.text.condition-rule.tooltip' })}>
+ <Input
+ value={conditionProps.data}
+ disabled={readonly}
+ placeholder={formatMessage({ id: 'component.plugin-flow.text.condition.placeholder' })}
+ onChange={e => {
+ setConditionProps({
+ ...conditionProps,
+ data: e.target.value
+ })
+ }}
+ />
+ </Form.Item>
+ </Modal>
+ </React.Fragment>
+ )
+}
+
+export default PluginFlow
diff --git a/web/src/components/PluginFlow/components/FlowGraph/FlowGraph.ts b/web/src/components/PluginFlow/components/FlowGraph/FlowGraph.ts
new file mode 100644
index 0000000..af9fc32
--- /dev/null
+++ b/web/src/components/PluginFlow/components/FlowGraph/FlowGraph.ts
@@ -0,0 +1,394 @@
+/*
+ * 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 { Graph, Addon, FunctionExt } from '@antv/x6'
+import type { Model, Cell } from '@antv/x6'
+import { formatMessage } from 'umi'
+import { notification } from 'antd'
+
+import './shapes'
+import { DEFAULT_OPINIONS, DEFAULT_PLUGIN_FLOW_DATA, DEFAULT_STENCIL_OPINIONS, FlowGraphEvent, FlowGraphShape } from '../../constants'
+
+class FlowGraph {
+ public static graph: Graph
+ private static stencil: Addon.Stencil
+ private static pluginTypeList: string[] = []
+ private static plugins: PluginComponent.Meta[] = []
+
+ public static init(container: HTMLElement, plugins: PluginComponent.Meta[] = [], chart: Model.FromJSONData) {
+ this.graph = new Graph({
+ container,
+ ...DEFAULT_OPINIONS
+ })
+
+ this.plugins = plugins
+ this.pluginTypeList = Array.from(new Set(plugins.map(item => item.type)))
+
+ this.initStencil()
+ this.initShape()
+ this.initGraphShape(chart)
+ this.initEvent()
+ return this.graph
+ }
+
+ // NOTE: set cell data according to Cell ID
+ public static setData(id: string, data: any): void {
+ const cell = this.graph.getCell(id)
+ if (cell) {
+ cell.setData(data, { overwrite: true })
+ }
+ }
+
+ // NOTE: Generate groups for stencil
+ private static generateGroups(): Addon.Stencil.Group[] {
+ const otherGroupList = [{
+ name: 'basic',
+ title: formatMessage({ id: 'component.plugin-flow.text.general' }),
+ graphHeight: 104,
+ }]
+
+ const pluginGroupList = this.pluginTypeList.map(item => {
+ const count = this.plugins.filter(plugin => plugin.type === item).length
+ return {
+ name: item,
+ title: formatMessage({ id: `component.plugin.${item}` }),
+ layoutOptions: {
+ columns: 1,
+ marginX: 60,
+ },
+ graphHeight: count * 82,
+ }
+ })
+
+ return otherGroupList.concat(pluginGroupList)
+ }
+
+ private static initStencil() {
+ this.stencil = new Addon.Stencil({
+ target: this.graph,
+ ...DEFAULT_STENCIL_OPINIONS,
+ groups: this.generateGroups()
+ })
+ const stencilContainer = document.querySelector('#stencil')
+ stencilContainer?.appendChild(this.stencil.container)
+ }
+
+ private static initShape() {
+ const { graph } = this
+ const r1 = graph.createNode({
+ shape: FlowGraphShape.start,
+ attrs: {
+ body: {
+ rx: 24,
+ ry: 24,
+ },
+ text: {
+ textWrap: {
+ text: formatMessage({ id: 'component.plugin-flow.text.start-node' }),
+ },
+ },
+ },
+ })
+
+ const r3 = graph.createNode({
+ shape: FlowGraphShape.condition,
+ width: 58,
+ height: 58,
+ angle: 45,
+ attrs: {
+ text: {
+ textWrap: {
+ text: formatMessage({ id: 'component.plugin-flow.text.condition2' }),
+ },
+ transform: 'rotate(-45deg)',
+ },
+ },
+ ports: {
+ groups: {
+ top: {
+ position: {
+ name: 'top',
+ args: {
+ dx: -26,
+ },
+ },
+ },
+ right: {
+ position: {
+ name: 'right',
+ args: {
+ dy: -26,
+ },
+ },
+ },
+ bottom: {
+ position: {
+ name: 'bottom',
+ args: {
+ dx: 26,
+ },
+ },
+ },
+ left: {
+ position: {
+ name: 'left',
+ args: {
+ dy: 26,
+ },
+ },
+ },
+ },
+ },
+ })
+
+ this.stencil.load([r1, r3], 'basic')
+ this.pluginTypeList.forEach(type => {
+ const plugins = this.plugins.filter(plugin => plugin.type === type).map(plugin => {
+ return graph.createNode({
+ shape: FlowGraphShape.plugin,
+ attrs: {
+ title: {
+ text: plugin.name
+ },
+ text: {
+ text: plugin.name
+ }
+ }
+ })
+ })
+
+ this.stencil.load(plugins, type)
+ })
+ }
+
+ private static initGraphShape(chart: Model.FromJSONData) {
+ if (!chart) {
+ return
+ }
+ this.graph.fromJSON(chart)
+ }
+
+ private static showPorts(ports: NodeListOf<SVGAElement>, show: boolean) {
+ // eslint-disable-next-line
+ for (let i = 0, len = ports.length; i < len; i = i + 1) {
+ // eslint-disable-next-line
+ ports[i].style.visibility = show ? 'visible' : 'hidden'
+ }
+ }
+
+ private static initEvent() {
+ const { graph } = this
+ const container = document.getElementById('container')!
+
+ graph.on(
+ 'node:mouseenter',
+ FunctionExt.debounce(() => {
+ const ports = container.querySelectorAll(
+ '.x6-port-body',
+ ) as NodeListOf<SVGAElement>
+ this.showPorts(ports, true)
+ }),
+ 500,
+ )
+
+ graph.on('node:mouseleave', () => {
+ const ports = container.querySelectorAll(
+ '.x6-port-body',
+ ) as NodeListOf<SVGAElement>
+ this.showPorts(ports, false)
+ })
+
+ graph.on('node:dblclick', ({ node }) => {
+ if (node.shape === FlowGraphShape.plugin) {
+ const name = node.getAttrByPath('text/text') as string
+ if (!name) {
+ return
+ }
+
+ this.graph.trigger(FlowGraphEvent.PLUGIN_CHANGE, {
+ visible: true,
+ id: node.id,
+ name,
+ data: node.getData()
+ })
+ }
+
+ if (node.shape === FlowGraphShape.condition) {
+ this.graph.trigger(FlowGraphEvent.CONDITION_CHANGE, {
+ id: node.id,
+ data: node.getData(),
+ visible: true
+ })
+ }
+ })
+
+ graph.bindKey('backspace', () => {
+ const cells = graph.getSelectedCells()
+ if (cells.length) {
+ graph.removeCells(cells)
+ }
+ })
+ }
+
+ private static getNextCell(id = '', position = ''): Cell.Properties | undefined {
+ const { cells = [] } = this.graph.toJSON()
+ const cell = cells.find(item => item.id === id)
+ if (!cell) {
+ return undefined
+ }
+
+ if (!cell.ports) {
+ return undefined
+ }
+
+ const port = cell.ports.items.find((item: { group: string }) => item.group === position)
+ if (!port) {
+ return undefined
+ }
+
+ const targetCellId = cells.find(item => item.source?.port === port.id && item.source?.cell === id)?.target.cell
+ const targetCell = cells.find(item => item.id === targetCellId)
+ return targetCell
+ }
+
+ private static getLeafList(currentId = '') {
+ let ids: string[] = []
+
+ const fn = (id: string) => {
+ const cell = this.getNextCell(id, "right")
+ if (!cell || !cell.id) {
+ return
+ }
+ ids = ids.concat(cell.id);
+ fn(cell.id)
+ }
+
+ fn(currentId)
+ return [currentId].concat(ids)
+ }
+
+ /**
+ * Convert Graph JSON Data to API Request Body Data
+ */
+ public static convertToData(chart: typeof DEFAULT_PLUGIN_FLOW_DATA.chart | undefined = undefined): {
+ chart: {
+ cells: Cell.Properties[];
+ };
+ conf: Record<string, any>;
+ rule: Record<string, any>;
+ } | undefined {
+ const data = {
+ ...DEFAULT_PLUGIN_FLOW_DATA,
+ chart: chart || this.graph.toJSON()
+ }
+
+ const { cells = [] } = data.chart
+
+ const edgeCells = cells.filter(cell => cell.shape === 'edge')
+
+ const startCell = cells.find(cell => cell.shape === FlowGraphShape.start)
+ if (!startCell) {
+ notification.warn({
+ message: formatMessage({ id: 'component.plugin-flow.text.no-start-node' })
+ })
+ return
+ }
+
+ const rootCell = cells.find(cell => cell.shape === 'edge' && cell.source.cell === startCell.id)
+ if (!rootCell) {
+ notification.warn({
+ message: formatMessage({ id: 'component.plugin-flow.text.no-root-node' })
+ })
+ return
+ }
+ data.rule.root = rootCell.target.cell
+
+ // Get the ID associated with each node, the relationship between nodes is in edgeCells.
+ edgeCells.forEach(edge => {
+ const sourceId = edge.source.cell
+ const targetId = edge.target.cell
+
+ data.rule[sourceId] = []
+
+ this.getLeafList(targetId).forEach(id => {
+ const cell = cells.find(item => item.id === id)
+ if (!cell) {
+ return
+ }
+
+ if (cell.shape === FlowGraphShape.condition) {
+ const nextCell = this.getNextCell(cell.id, "bottom");
+ if (!nextCell) {
+ return
+ }
+ data.rule[sourceId].push([cell.data, nextCell.id])
+ }
+
+ if (cell.shape === FlowGraphShape.plugin) {
+ data.rule[sourceId].push(['', cell.id])
+ }
+ })
+ })
+
+ // NOTE: Omit empty array, or API will throw error.
+ Object.entries(data.rule).forEach(([key, value]) => {
+ if (value.length === 0) {
+ delete data.rule[key]
+ }
+
+ if (key === 'root') {
+ return
+ }
+ const cell = cells.find(item => item.id === key)
+ if (cell?.shape !== FlowGraphShape.plugin) {
+ delete data.rule[key]
+ }
+ })
+
+ const invalidPluginCell = cells.find(item => item.shape === FlowGraphShape.plugin && !item.data)
+ if (invalidPluginCell) {
+ notification.warn({
+ message: formatMessage({ id: 'component.plugin-flow.text.without-data' }),
+ description: `${formatMessage({ id: 'component.plugin-flow.text.plugin-without-data.description' })}${invalidPluginCell.attrs?.text.text}`
+ })
+ return
+ }
+
+ const invalidConditionCell = cells.find(item => item.shape === FlowGraphShape.condition && !item.data)
+ if (invalidConditionCell) {
+ notification.warn({
+ message: formatMessage({ id: 'component.plugin-flow.text.without-data' }),
+ description: `${formatMessage({ id: 'component.plugin-flow.text.condition-without-configuration' })}`
+ })
+ return
+ }
+
+ data.conf = {}
+ cells.filter(item => item.shape === FlowGraphShape.plugin && item.id).forEach(item => {
+ if (item.id) {
+ data.conf[item.id] = {
+ name: item.attrs?.text.text,
+ conf: item.data
+ }
+ }
+ })
+
+ // eslint-disable-next-line
+ return data
+ }
+}
+
+export default FlowGraph
diff --git a/web/src/components/PluginOrchestration/components/index.ts b/web/src/components/PluginFlow/components/FlowGraph/index.ts
similarity index 93%
rename from web/src/components/PluginOrchestration/components/index.ts
rename to web/src/components/PluginFlow/components/FlowGraph/index.ts
index cb920f2..5dc41d5 100644
--- a/web/src/components/PluginOrchestration/components/index.ts
+++ b/web/src/components/PluginFlow/components/FlowGraph/index.ts
@@ -14,5 +14,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export * from './Page';
-export * from './SidebarItem';
+export { default } from './FlowGraph'
diff --git a/web/src/components/PluginFlow/components/FlowGraph/shapes.ts b/web/src/components/PluginFlow/components/FlowGraph/shapes.ts
new file mode 100644
index 0000000..2e9756e
--- /dev/null
+++ b/web/src/components/PluginFlow/components/FlowGraph/shapes.ts
@@ -0,0 +1,161 @@
+/*
+ * 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 { Graph } from '@antv/x6'
+
+import defaultPluginImg from '../../../../../public/static/default-plugin.png';
+import { DEFAULT_SHAPE_RECT_OPINIONS, FlowGraphShape } from '../../constants';
+
+export const FlowChartRect = Graph.registerNode(FlowGraphShape.base, DEFAULT_SHAPE_RECT_OPINIONS)
+
+export const FlowChartConditionRect = Graph.registerNode(FlowGraphShape.condition, {
+ ...DEFAULT_SHAPE_RECT_OPINIONS,
+ ports: {
+ ...DEFAULT_SHAPE_RECT_OPINIONS.ports,
+ items: [
+ {
+ group: 'top',
+ },
+ {
+ group: 'right',
+ },
+ {
+ group: 'bottom',
+ },
+ ],
+ }
+})
+
+export const FlowChartStartRect = Graph.registerNode(FlowGraphShape.start, {
+ ...DEFAULT_SHAPE_RECT_OPINIONS,
+ ports: {
+ ...DEFAULT_SHAPE_RECT_OPINIONS.ports,
+ items: [
+ {
+ group: 'bottom',
+ },
+ ],
+ }
+})
+
+export const FlowChartEndRect = Graph.registerNode(FlowGraphShape.end, {
+ ...DEFAULT_SHAPE_RECT_OPINIONS,
+ ports: {
+ ...DEFAULT_SHAPE_RECT_OPINIONS.ports,
+ items: [
+ {
+ group: 'top',
+ },
+ ],
+ }
+})
+
+export const FlowChartPluginRect = Graph.registerNode(FlowGraphShape.plugin, {
+ inherit: 'rect',
+ width: 200,
+ height: 60,
+ attrs: {
+ body: {
+ stroke: '#5F95FF',
+ strokeWidth: 1,
+ fill: 'rgba(95,149,255,0.05)',
+ },
+ image: {
+ 'xlink:href':
+ defaultPluginImg,
+ width: 16,
+ height: 16,
+ x: 12,
+ y: 12,
+ },
+ title: {
+ text: 'Unknown Plugin',
+ refX: 40,
+ refY: 14,
+ fill: 'rgba(0,0,0,0.85)',
+ fontSize: 12,
+ 'text-anchor': 'start',
+ },
+ text: {
+ text: '',
+ refX: 40,
+ refY: 38,
+ fontSize: 12,
+ fill: 'rgba(0,0,0,0.6)',
+ 'text-anchor': 'start',
+ },
+ },
+ markup: [
+ {
+ tagName: 'rect',
+ selector: 'body',
+ },
+ {
+ tagName: 'image',
+ selector: 'image',
+ },
+ {
+ tagName: 'text',
+ selector: 'title',
+ },
+ {
+ tagName: 'text',
+ selector: 'text',
+ },
+ ],
+ ports: {
+ groups: {
+ top: {
+ position: 'top',
+ attrs: {
+ circle: {
+ r: 3,
+ magnet: true,
+ stroke: '#5F95FF',
+ strokeWidth: 1,
+ fill: '#fff',
+ style: {
+ visibility: 'hidden',
+ },
+ },
+ },
+ },
+ bottom: {
+ position: 'bottom',
+ attrs: {
+ circle: {
+ r: 3,
+ magnet: true,
+ stroke: '#5F95FF',
+ strokeWidth: 1,
+ fill: '#fff',
+ style: {
+ visibility: 'hidden',
+ },
+ },
+ },
+ },
+ },
+ items: [
+ {
+ group: 'top',
+ },
+ {
+ group: 'bottom',
+ }
+ ],
+ },
+})
diff --git a/web/src/components/PluginFlow/components/Toolbar/index.tsx b/web/src/components/PluginFlow/components/Toolbar/index.tsx
new file mode 100644
index 0000000..7931a26
--- /dev/null
+++ b/web/src/components/PluginFlow/components/Toolbar/index.tsx
@@ -0,0 +1,195 @@
+/*
+* MIT License
+
+* Copyright (c) 2019 Alipay.inc
+
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+import React, { useEffect, useState } from 'react'
+import { Toolbar } from '@antv/x6-react-components'
+import FlowGraph from '../FlowGraph'
+import { DataUri } from '@antv/x6'
+import {
+ ClearOutlined,
+ SaveOutlined,
+ PrinterOutlined,
+ UndoOutlined,
+ RedoOutlined,
+ CopyOutlined,
+ ScissorOutlined,
+ SnippetsOutlined,
+} from '@ant-design/icons'
+import '@antv/x6-react-components/es/toolbar/style/index.css'
+
+const { Item, Group } = Toolbar
+
+const ToolbarComponent = () => {
+ const [canUndo, setCanUndo] = useState(false)
+ const [canRedo, setCanRedo] = useState(false)
+
+ const copy = () => {
+ const { graph } = FlowGraph
+ const cells = graph.getSelectedCells()
+ if (cells.length) {
+ graph.copy(cells)
+ }
+ return false
+ }
+
+ const cut = () => {
+ const { graph } = FlowGraph
+ const cells = graph.getSelectedCells()
+ if (cells.length) {
+ graph.cut(cells)
+ }
+ return false
+ }
+
+ const paste = () => {
+ const { graph } = FlowGraph
+ if (!graph.isClipboardEmpty()) {
+ const cells = graph.paste({ offset: 32 })
+ graph.cleanSelection()
+ graph.select(cells)
+ }
+ return false
+ }
+
+ useEffect(() => {
+ const { graph } = FlowGraph
+ const { history } = graph
+ setCanUndo(history.canUndo())
+ setCanRedo(history.canRedo())
+ history.on('change', () => {
+ setCanUndo(history.canUndo())
+ setCanRedo(history.canRedo())
+ })
+
+ graph.bindKey(['meta+z', 'ctrl+z'], () => {
+ if (history.canUndo()) {
+ history.undo()
+ }
+ return false
+ })
+ graph.bindKey(['meta+shift+z', 'ctrl+y'], () => {
+ if (history.canRedo()) {
+ history.redo()
+ }
+ return false
+ })
+ graph.bindKey(['meta+d', 'ctrl+d'], () => {
+ graph.clearCells()
+ return false
+ })
+ graph.bindKey(['meta+s', 'ctrl+s'], () => {
+ graph.toPNG((datauri: string) => {
+ DataUri.downloadDataUri(datauri, 'chart.png')
+ })
+ return false
+ })
+ graph.bindKey(['meta+p', 'ctrl+p'], () => {
+ graph.printPreview()
+ return false
+ })
+ graph.bindKey(['meta+c', 'ctrl+c'], copy)
+ graph.bindKey(['meta+v', 'ctrl+v'], paste)
+ graph.bindKey(['meta+x', 'ctrl+x'], cut)
+ }, [])
+
+ const handleClick = (name: string) => {
+ const { graph } = FlowGraph
+ switch (name) {
+ case 'undo':
+ graph.history.undo()
+ break
+ case 'redo':
+ graph.history.redo()
+ break
+ case 'delete':
+ graph.clearCells()
+ break
+ case 'save':
+ graph.toPNG((datauri: string) => {
+ DataUri.downloadDataUri(datauri, 'chart.png')
+ })
+ break
+ case 'print':
+ graph.printPreview()
+ break
+ case 'copy':
+ copy()
+ break
+ case 'cut':
+ cut()
+ break
+ case 'paste':
+ paste()
+ break
+ default:
+ break
+ }
+ }
+
+ return (
+ <div>
+ <Toolbar hoverEffect={true} size="small" onClick={handleClick}>
+ <Group>
+ <Item
+ name="delete"
+ icon={<ClearOutlined />}
+ tooltip="Clear (Cmd + D, Ctrl + D)"
+ />
+ </Group>
+ <Group>
+ <Item
+ name="undo"
+ tooltip="Undo (Cmd + Z, Ctrl + Z)"
+ icon={<UndoOutlined />}
+ disabled={!canUndo}
+ />
+ <Item
+ name="redo"
+ tooltip="Redo (Cmd + Shift + Z, Ctrl + Y)"
+ icon={<RedoOutlined />}
+ disabled={!canRedo}
+ />
+ </Group>
+ <Group>
+ <Item name="copy" tooltip="Copy (Cmd + C, Ctrl + C)" icon={<CopyOutlined />} />
+ <Item name="cut" tooltip="Cut (Cmd + X, Ctrl + X)" icon={<ScissorOutlined />} />
+ <Item
+ name="paste"
+ tooltip="Paste (Cmd + V, Ctrl + V)"
+ icon={<SnippetsOutlined />}
+ />
+ </Group>
+ <Group>
+ <Item name="save" icon={<SaveOutlined />} tooltip="Save (Cmd + S, Ctrl + S)" />
+ <Item
+ name="print"
+ icon={<PrinterOutlined />}
+ tooltip="Print (Cmd + P, Ctrl + P)"
+ />
+ </Group>
+ </Toolbar>
+ </div>
+ )
+}
+
+export default ToolbarComponent
diff --git a/web/src/components/PluginFlow/constants.ts b/web/src/components/PluginFlow/constants.ts
new file mode 100644
index 0000000..b81e60c
--- /dev/null
+++ b/web/src/components/PluginFlow/constants.ts
@@ -0,0 +1,294 @@
+/*
+ * 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 { Shape, Dom } from '@antv/x6'
+import type { Addon, Graph, Cell } from '@antv/x6'
+import { formatMessage } from '@/.umi/plugin-locale/localeExports'
+
+export const DEFAULT_STENCIL_WIDTH = 280
+export const DEFAULT_TOOLBAR_HEIGHT = 38
+export const DEFAULT_SHAPE_TEXT_EDIT_CLASS_NAME = ".flow-graph-shape-text-editor"
+
+export const DEFAULT_OPINIONS: Partial<Graph.Options> = {
+ scroller: true,
+ width: 800,
+ height: 600,
+ grid: {
+ size: 10,
+ visible: true,
+ },
+ selecting: {
+ enabled: true,
+ multiple: true,
+ rubberband: true,
+ movable: true,
+ showNodeSelectionBox: true,
+ filter: ['groupNode'],
+ },
+ connecting: {
+ allowBlank: false,
+ highlight: true,
+ snap: true,
+ createEdge() {
+ return new Shape.Edge({
+ attrs: {
+ line: {
+ stroke: '#5F95FF',
+ strokeWidth: 1,
+ targetMarker: {
+ name: 'classic',
+ size: 8,
+ },
+ },
+ },
+ router: {
+ name: 'manhattan',
+ },
+ zIndex: 0,
+ })
+ },
+ validateConnection({
+ sourceView,
+ targetView,
+ sourceMagnet,
+ targetMagnet,
+ }) {
+ if (sourceView === targetView) {
+ return false
+ }
+ if (!sourceMagnet) {
+ return false
+ }
+ if (!targetMagnet) {
+ return false
+ }
+ return true
+ },
+ },
+ highlighting: {
+ magnetAvailable: {
+ name: 'stroke',
+ args: {
+ padding: 4,
+ attrs: {
+ strokeWidth: 4,
+ stroke: 'rgba(223,234,255)',
+ },
+ },
+ },
+ },
+ snapline: true,
+ history: true,
+ clipboard: {
+ enabled: true,
+ },
+ keyboard: {
+ enabled: true,
+ },
+ embedding: {
+ enabled: true,
+ findParent({ node }) {
+ const bbox = node.getBBox()
+ return this.getNodes().filter((item) => {
+ const data = item.getData<any>()
+ if (data && data.parent) {
+ const targetBBox = item.getBBox()
+ return bbox.isIntersectWithRect(targetBBox)
+ }
+ return false
+ })
+ },
+ },
+}
+
+export const DEFAULT_STENCIL_OPINIONS: Partial<Addon.Stencil.Options> = {
+ title: formatMessage({ id: 'component.plugin-flow.text.nodes-area' }),
+ stencilGraphWidth: DEFAULT_STENCIL_WIDTH,
+ search: (cell, keyword) => {
+ if (keyword) {
+ return (cell as any).label?.indexOf(keyword) !== -1
+ }
+ return true
+ },
+ notFoundText: formatMessage({ id: 'component.plugin-flow.text.nodes.not-found' }),
+ placeholder: formatMessage({ id: 'component.plugin-flow.text.search-nodes.placeholder' }),
+ collapsable: true,
+}
+
+export const DEFAULT_SHAPE_RECT_OPINIONS = {
+ inherit: 'rect',
+ width: 80,
+ height: 42,
+ attrs: {
+ body: {
+ stroke: '#5F95FF',
+ strokeWidth: 1,
+ fill: 'rgba(95,149,255,0.05)',
+ },
+ fo: {
+ refWidth: '100%',
+ refHeight: '100%',
+ },
+ foBody: {
+ xmlns: Dom.ns.xhtml,
+ style: {
+ width: '100%',
+ height: '100%',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ },
+ text: {
+ fontSize: 12,
+ fill: 'rgba(0,0,0,0.85)',
+ textWrap: {
+ text: '',
+ width: -10,
+ },
+ },
+ },
+ markup: [
+ {
+ tagName: 'rect',
+ selector: 'body',
+ },
+ {
+ tagName: 'text',
+ selector: 'text',
+ }
+ ],
+ ports: {
+ groups: {
+ top: {
+ position: 'top',
+ attrs: {
+ circle: {
+ r: 3,
+ magnet: true,
+ stroke: '#5F95FF',
+ strokeWidth: 1,
+ fill: '#fff',
+ style: {
+ visibility: 'hidden',
+ },
+ },
+ },
+ },
+ right: {
+ position: 'right',
+ attrs: {
+ circle: {
+ r: 3,
+ magnet: true,
+ stroke: '#5F95FF',
+ strokeWidth: 1,
+ fill: '#fff',
+ style: {
+ visibility: 'hidden',
+ },
+ },
+ },
+ },
+ bottom: {
+ position: 'bottom',
+ attrs: {
+ circle: {
+ r: 3,
+ magnet: true,
+ stroke: '#5F95FF',
+ strokeWidth: 1,
+ fill: '#fff',
+ style: {
+ visibility: 'hidden',
+ },
+ },
+ },
+ },
+ left: {
+ position: 'left',
+ attrs: {
+ circle: {
+ r: 3,
+ magnet: true,
+ stroke: '#5F95FF',
+ strokeWidth: 1,
+ fill: '#fff',
+ style: {
+ visibility: 'hidden',
+ },
+ },
+ },
+ },
+ },
+ items: [
+ {
+ group: 'top',
+ },
+ {
+ group: 'right',
+ },
+ {
+ group: 'bottom',
+ },
+ {
+ group: 'left',
+ },
+ ],
+ },
+}
+
+export enum FlowGraphShape {
+ base = 'flow-chart-rect',
+ condition = 'flow-chart-condition-rect',
+ start = 'flow-chart-start-rect',
+ end = 'flow-chart-end-rect',
+ plugin = 'flow-chart-plugin-rect'
+}
+
+export enum FlowGraphEvent {
+ PLUGIN_CHANGE = 'flowgraph:change:plugin',
+ CONDITION_CHANGE = 'flowgraph:change:condition',
+}
+
+export const DEFAULT_PLUGIN_PROPS = {
+ id: '',
+ name: '',
+ visible: false,
+ data: {}
+}
+
+export const DEFAULT_CONDITION_PROPS = {
+ visible: false,
+ id: '',
+ data: ''
+}
+
+export const DEFAULT_PLUGIN_FLOW_DATA: {
+ chart: {
+ cells: Cell.Properties[];
+ };
+ conf: Record<string, any>;
+ rule: Record<string, any>;
+} = {
+ chart: {
+ cells: []
+ },
+ conf: {},
+ rule: {
+ root: ""
+ }
+}
diff --git a/web/src/components/PluginOrchestration/components/Page.tsx b/web/src/components/PluginFlow/index.ts
similarity index 82%
rename from web/src/components/PluginOrchestration/components/Page.tsx
rename to web/src/components/PluginFlow/index.ts
index 2bdc3ff..fd368ef 100644
--- a/web/src/components/PluginOrchestration/components/Page.tsx
+++ b/web/src/components/PluginFlow/index.ts
@@ -14,7 +14,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import * as React from 'react';
-import { SPageContent } from '../DrawPluginStyle';
-
-export const Page: React.FC = (props) => <SPageContent>{props.children}</SPageContent>;
+export { default } from './PluginFlow'
diff --git a/web/src/components/PluginFlow/locales/en-US.ts b/web/src/components/PluginFlow/locales/en-US.ts
new file mode 100644
index 0000000..fdec09a
--- /dev/null
+++ b/web/src/components/PluginFlow/locales/en-US.ts
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+export default {
+ 'component.plugin-flow.text.condition.required': 'Configure Rule',
+ 'component.plugin-flow.text.condition': 'Rule',
+ 'component.plugin-flow.text.condition2': 'Condition',
+ 'component.plugin-flow.text.condition.placeholder': 'Please enter the rule',
+ 'component.plugin-flow.text.without-data': 'Found node without configuration',
+ 'component.plugin-flow.text.plugin-without-data.description': 'Please condigure plugin: ',
+ 'component.plugin-flow.text.no-start-node': 'Please connect the start node',
+ 'component.plugin-flow.text.no-root-node': 'Root node not found',
+ 'component.plugin-flow.text.start-node': 'Start',
+ 'component.plugin-flow.text.general': 'General',
+ 'component.plugin-flow.text.nodes-area': 'Available Nodes',
+ 'component.plugin-flow.text.nodes.not-found': 'Not Found',
+ 'component.plugin-flow.text.search-nodes.placeholder': 'Search plugin by name',
+ 'component.plugin-flow.text.condition-rule.tooltip': 'The judgment condition of the node. e.g: code == 503',
+ 'component.plugin-flow.text.line': 'Line',
+ 'component.plugin-flow.text.grid': 'Grid',
+ 'component.plugin-flow.text.background': 'Background',
+ 'component.plugin-flow.text.node': 'Node',
+ 'component.plugin-flow.text.text': 'Text',
+ 'component.plugin-flow.text.condition-without-configuration': 'Please check all condition nodes\' data',
+ 'component.plugin-flow.text.preview.readonly': 'NOTE: your actions on the following drawer will not be preserved.',
+ 'component.plugin-flow.text.both-modes-exist': 'The orchestration mode configuration will override the normal mode configuration, are you sure to continue?',
+ 'component.plugin-flow.text.both-modes-exist.title': 'Attention'
+}
diff --git a/web/src/components/PluginFlow/locales/zh-CN.ts b/web/src/components/PluginFlow/locales/zh-CN.ts
new file mode 100644
index 0000000..4486dff
--- /dev/null
+++ b/web/src/components/PluginFlow/locales/zh-CN.ts
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+export default {
+ 'component.plugin-flow.text.condition.required': '配置判断条件',
+ 'component.plugin-flow.text.condition': '判断条件',
+ 'component.plugin-flow.text.condition2': '条件判断',
+ 'component.plugin-flow.text.condition.placeholder': '请输入判断条件',
+ 'component.plugin-flow.text.without-data': '存在未配置的元件',
+ 'component.plugin-flow.text.plugin-without-data.description': '请配置插件:',
+ 'component.plugin-flow.text.no-start-node': '请关联开始节点',
+ 'component.plugin-flow.text.no-root-node': '未找到根节点',
+ 'component.plugin-flow.text.start-node': '开始',
+ 'component.plugin-flow.text.general': '通用',
+ 'component.plugin-flow.text.nodes-area': '元件选择区',
+ 'component.plugin-flow.text.nodes.not-found': '无匹配元件',
+ 'component.plugin-flow.text.search-nodes.placeholder': '请输入插件元件名称',
+ 'component.plugin-flow.text.condition-rule.tooltip': '节点的判断条件。例如:code == 503',
+ 'component.plugin-flow.text.line': '线条',
+ 'component.plugin-flow.text.grid': '网格',
+ 'component.plugin-flow.text.background': '背景',
+ 'component.plugin-flow.text.node': '节点',
+ 'component.plugin-flow.text.text': '文本',
+ 'component.plugin-flow.text.condition-without-configuration': '请检查条件判断元件的配置',
+ 'component.plugin-flow.text.preview.readonly': '请注意:在当前页面,您在画布上地操作不会被保留。',
+ 'component.plugin-flow.text.both-modes-exist': '编排模式配置将覆盖普通模式配置,是否继续操作?',
+ 'component.plugin-flow.text.both-modes-exist.title': '配置冲突'
+}
diff --git a/web/src/components/PluginOrchestration/typing.d.ts b/web/src/components/PluginFlow/style.less
similarity index 64%
rename from web/src/components/PluginOrchestration/typing.d.ts
rename to web/src/components/PluginFlow/style.less
index 3f4e30c..41ccddd 100644
--- a/web/src/components/PluginOrchestration/typing.d.ts
+++ b/web/src/components/PluginFlow/style.less
@@ -14,13 +14,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export declare namespace PluginOrchestrationModule {
- type Meta = {
- name: string;
- priority: number;
- schema: Record<string, any>;
- type: string;
- version: number;
- consumer_schema?: Record<string, any>;
- };
+
+.container {
+ display: flex;
+ height: calc(100% - 48px);
+}
+
+.stencil {
+ position: relative;
+ // NOTE: constants.ts -> DEFAULT_STENCIL_WIDTH
+ width: 280px;
+ height: 400px;
+ border-right: 1px solid rgba(0, 0, 0, 0.08);
+}
+
+.panel {
+ height: 100%;
+}
+
+.toolbar {
+ display: flex;
+ align-items: center;
+ // NOTE: DEFAULT_TOOLBAR_HEIGHT
+ height: 38px;
+ background-color: #f7f9fb;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
diff --git a/web/src/components/PluginOrchestration/DrawPluginStyle.ts b/web/src/components/PluginOrchestration/DrawPluginStyle.ts
deleted file mode 100644
index a1d40b2..0000000
--- a/web/src/components/PluginOrchestration/DrawPluginStyle.ts
+++ /dev/null
@@ -1,82 +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.
- */
-import styled from 'styled-components';
-
-export const SOuter = styled.div`
- padding: 30px;
-`;
-
-export const SInput = styled.input`
- padding: 10px;
- border: 1px solid cornflowerblue;
- width: 100%;
-`;
-
-export const SMessage = styled.div`
- margin: 10px;
- padding: 10px;
- line-height: 1.4em;
-`;
-
-export const SButton = styled.div`
- padding: 10px 15px;
- background: cornflowerblue;
- color: white;
- border-radius: 3px;
- text-align: center;
- transition: 0.3s ease all;
- cursor: pointer;
- &:hover {
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
- }
- &:active {
- background: #5682d2;
- }
-`;
-
-export const SPortDefaultOuter = styled.div`
- width: 24px;
- height: 24px;
- background: cornflowerblue;
- cursor: pointer;
- display: flex;
- justify-content: center;
- align-items: center;
-`;
-
-export const SContent = styled.div`
- display: flex;
- flex-direction: column;
- flex: 1;
- overflow: hidden;
-`;
-
-export const SSidebar = styled.div`
- width: 300px;
- background: white;
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
-`;
-
-export const SPageContent = styled.div`
- display: flex;
- flex-direction: row;
- flex: 1;
- max-width: 100vw;
- max-height: 100vh;
-`;
diff --git a/web/src/components/PluginOrchestration/components/SidebarItem.tsx b/web/src/components/PluginOrchestration/components/SidebarItem.tsx
deleted file mode 100644
index 5a4b380..0000000
--- a/web/src/components/PluginOrchestration/components/SidebarItem.tsx
+++ /dev/null
@@ -1,42 +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.
- */
-import * as React from 'react';
-import { REACT_FLOW_CHART } from '@mrblenny/react-flow-chart';
-import type { INode } from '@mrblenny/react-flow-chart';
-import { Button } from 'antd';
-
-import { SOuter } from '../DrawPluginStyle';
-
-export type ISidebarItemProps = {
- type: string;
- ports: INode['ports'];
- properties?: any;
-};
-
-export const SidebarItem: React.FC<ISidebarItemProps> = ({ type, ports, properties }) => {
- return (
- <SOuter
- draggable
- onDragStart={(event: any) => {
- event.dataTransfer.setData(REACT_FLOW_CHART, JSON.stringify({ type, ports, properties }));
- }}
- style={{ padding: '5px' }}
- >
- <Button type="dashed">{type}</Button>
- </SOuter>
- );
-};
diff --git a/web/src/components/PluginOrchestration/constants.ts b/web/src/components/PluginOrchestration/constants.ts
deleted file mode 100644
index 2a8c97c..0000000
--- a/web/src/components/PluginOrchestration/constants.ts
+++ /dev/null
@@ -1,62 +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.
- */
-export const INIT_CHART = {
- offset: { x: 0, y: 0 },
- scale: 0.577,
- nodes: {},
- links: {},
- selected: {},
- hovered: {},
-};
-
-export const PLUGINS_PORTS = {
- port1: {
- id: 'port1',
- type: 'input',
- properties: {
- custom: 'property',
- },
- },
- port2: {
- id: 'port2',
- type: 'output',
- properties: {
- custom: 'property',
- },
- },
-};
-
-export const CONDITION_PORTS = {
- port1: {
- id: 'port1',
- type: 'input',
- },
- port2: {
- id: 'port2',
- type: 'output',
- properties: {
- value: 'no',
- },
- },
- port3: {
- id: 'port3',
- type: 'output',
- properties: {
- value: 'yes',
- },
- },
-};
diff --git a/web/src/components/PluginOrchestration/customConfig.tsx b/web/src/components/PluginOrchestration/customConfig.tsx
deleted file mode 100644
index b438aa5..0000000
--- a/web/src/components/PluginOrchestration/customConfig.tsx
+++ /dev/null
@@ -1,77 +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.
- */
-import React from 'react';
-import { useIntl } from 'umi';
-import type { INodeInnerDefaultProps, IPortDefaultProps } from '@mrblenny/react-flow-chart';
-
-import { SOuter, SPortDefaultOuter } from './DrawPluginStyle';
-import { PanelType } from './index';
-
-export const NodeInnerCustom = ({ node }: INodeInnerDefaultProps) => {
- const { formatMessage } = useIntl();
- const { customData } = node.properties;
- if (customData.type === PanelType.Condition) {
- return (
- <SOuter>
- <p>
- {formatMessage({ id: 'page.panel.condition.name' })}:
- {customData.name || `(${formatMessage({ id: 'page.panel.condition.tips' })})`}
- </p>
- </SOuter>
- );
- }
-
- if (customData.type === PanelType.Plugin) {
- return (
- <SOuter>
- <p>
- {formatMessage({ id: 'page.panel.plugin.name' })}:{' '}
- {customData.name || `(${formatMessage({ id: 'page.panel.plugin.tips' })})`}
- </p>
- </SOuter>
- );
- }
-
- return (
- <SOuter>
- <br />
- </SOuter>
- );
-};
-
-export const PortCustom = (props: IPortDefaultProps) => (
- <SPortDefaultOuter>
- {props.port.properties && props.port.properties.value === 'yes' && (
- <svg style={{ width: '24px', height: '24px' }} viewBox="0 0 24 24">
- <path fill="white" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
- </svg>
- )}
- {props.port.properties && props.port.properties.value === 'no' && (
- <svg style={{ width: '24px', height: '24px' }} viewBox="0 0 24 24">
- <path
- fill="white"
- d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
- />
- </svg>
- )}
- {!props.port.properties && (
- <svg style={{ width: '24px', height: '24px' }} viewBox="0 0 24 24">
- <path fill="white" d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
- </svg>
- )}
- </SPortDefaultOuter>
-);
diff --git a/web/src/components/PluginOrchestration/index.tsx b/web/src/components/PluginOrchestration/index.tsx
deleted file mode 100644
index 4cff7ba..0000000
--- a/web/src/components/PluginOrchestration/index.tsx
+++ /dev/null
@@ -1,289 +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.
- */
-import React, { Fragment, useState, useEffect } from 'react';
-import { cloneDeep } from 'lodash';
-import { FlowChart } from '@mrblenny/react-flow-chart';
-import type { IFlowChartCallbacks } from '@mrblenny/react-flow-chart';
-import * as actions from '@mrblenny/react-flow-chart/src/container/actions';
-import { Form, Input, Button, Divider, Card, Select } from 'antd';
-import { withTheme } from '@rjsf/core';
-import { useIntl } from 'umi';
-
-// @ts-ignore
-import { Theme as AntDTheme } from '@rjsf/antd';
-
-import { Page, SidebarItem } from './components';
-import { INIT_CHART, PLUGINS_PORTS, CONDITION_PORTS } from './constants';
-import { SMessage, SContent, SSidebar } from './DrawPluginStyle';
-import { PortCustom, NodeInnerCustom } from './customConfig';
-import { fetchList } from './service';
-import type { PluginOrchestrationModule } from './typing';
-
-export * from './transform';
-
-export enum PanelType {
- Plugin,
- Condition,
- Default,
-}
-
-type Props = {
- data: any;
- onChange: (data: Record<string, unknown>) => void;
- readonly: boolean;
-};
-
-const PluginForm = withTheme(AntDTheme);
-
-const LAYOUT = {
- labelCol: { span: 8 },
- wrapperCol: { span: 16 },
-};
-const TAIL_LAYOUT = {
- wrapperCol: { offset: 8, span: 16 },
-};
-
-const SelectedSidebar: React.FC<Props> = ({ data = {}, onChange, readonly = false }) => {
- const [form] = Form.useForm();
- const [chart, setChart] = useState(cloneDeep(Object.keys(data).length ? data : INIT_CHART));
- const [schema, setSchema] = useState<any>();
- const [selectedType, setSelectedType] = useState<PanelType>(PanelType.Default);
- const [pluginList, setPluginList] = useState<PluginOrchestrationModule.Meta[]>([]);
- const [pluginCategory, setPluginCategory] = useState('All');
- const [showList, setShowList] = useState<string[]>();
- const [typeList, setTypeList] = useState<string[]>([]);
-
- const { formatMessage } = useIntl();
-
- const getCustomDataById = (id = chart.selected.id) => {
- if (!id || !chart.nodes[id].properties) {
- return {};
- }
- return chart.nodes[id].properties.customData;
- };
-
- const stateActionCallbacks = Object.keys(actions).reduce((obj, key) => {
- const clonedObj = cloneDeep(obj);
- clonedObj[key] = (...args: any) => {
- const action = actions[key];
- const newChartTransformer = action(...args);
- const newChart = newChartTransformer(chart);
- if (
- ['onLinkMouseEnter', 'onLinkMouseLeave', 'onNodeMouseEnter', 'onNodeMouseLeave'].includes(
- key,
- )
- ) {
- return newChart;
- }
-
- if (key === 'onDragCanvasStop') {
- setSelectedType(PanelType.Default);
- return newChart;
- }
-
- setChart({ ...chart, ...newChart });
- if (['onCanvasDrop', 'onNodeClick'].includes(key)) {
- const { type, name } = getCustomDataById(args.nodeId);
- setSelectedType(type);
- if (type === PanelType.Plugin && name) {
- const plugin = pluginList.find((item) => item.name === name);
- if (plugin) {
- setSchema(plugin.schema);
- }
- }
- }
- onChange(newChart);
- return newChart;
- };
- return clonedObj;
- }, {}) as IFlowChartCallbacks;
-
- const firstUpperCase = ([first, ...rest]: string) => first.toUpperCase() + rest.join('');
- useEffect(() => {
- fetchList().then((list) => {
- const categoryList: string[] = [];
- list.forEach((item) => {
- if (!categoryList.includes(firstUpperCase(item.type))) {
- categoryList.push(firstUpperCase(item.type));
- }
- });
- setTypeList(['All', ...categoryList.sort()]);
- setPluginList(list);
- setShowList(list.map((item) => item.name).sort());
- });
- }, []);
-
- const renderSidebar = () => {
- if (selectedType === PanelType.Condition) {
- form.setFieldsValue({ condition: getCustomDataById().name });
- return (
- <SMessage>
- <Form
- {...LAYOUT}
- name="basic"
- form={form}
- onFinish={(values) => {
- const clonedChart = cloneDeep(chart);
- clonedChart.nodes[chart.selected.id!].properties.customData.name = values.condition;
- setChart(clonedChart);
- onChange(clonedChart);
- setSelectedType(PanelType.Default);
- }}
- >
- <Form.Item
- label={formatMessage({ id: 'page.siderBar.form.label.panelType.condition' })}
- name="condition"
- rules={[
- {
- required: true,
- message: formatMessage({ id: 'page.siderBar.form.rule.panelType.condition' }),
- },
- ]}
- >
- <Input />
- </Form.Item>
- <Form.Item {...TAIL_LAYOUT}>
- <Button type="primary" htmlType="submit">
- {formatMessage({ id: 'page.siderBar.button.submit' })}
- </Button>
- </Form.Item>
- </Form>
- </SMessage>
- );
- }
- if (selectedType === PanelType.Plugin && schema) {
- return (
- <SMessage style={{ overflow: 'scroll' }}>
- <PluginForm
- schema={schema}
- liveValidate
- formData={getCustomDataById().data || {}}
- showErrorList={false}
- onSubmit={({ formData }) => {
- const clonedChart = cloneDeep(chart);
- clonedChart.nodes[chart.selected.id!].properties.customData.data = formData;
- setChart(clonedChart);
- onChange(clonedChart);
- setSelectedType(PanelType.Default);
- }}
- >
- {/* NOTE: Leave blank to hide the Submit button */}
- <Fragment />
-
- <Button type="primary" htmlType="submit">
- {formatMessage({ id: 'page.siderBar.button.submit' })}
- </Button>
- </PluginForm>
- </SMessage>
- );
- }
-
- return (
- <SSidebar>
- <SMessage style={{ fontSize: '16px', fontWeight: 'bold' }}>
- {formatMessage({ id: 'page.siderBar.tips' })}
- </SMessage>
- <Divider style={{ margin: '0px' }} />
- <SidebarItem
- type={formatMessage({ id: 'page.siderBar.form.label.panelType.condition' })}
- ports={CONDITION_PORTS}
- properties={{
- customData: {
- type: PanelType.Condition,
- },
- }}
- />
- <Divider orientation="left">{formatMessage({ id: 'page.siderBar.plugin' })}</Divider>
- <Select
- showSearch
- placeholder={formatMessage({ id: 'page.siderBar.form.label.panelType.plugin' })}
- optionFilterProp="children"
- defaultValue={pluginCategory}
- onChange={(value) => {
- setPluginCategory(value);
- if (value === 'All') {
- setShowList(pluginList.map((item) => item.name).sort());
- return;
- }
- setShowList(
- pluginList
- .filter((item) => item.type === value.toLowerCase())
- .map((item) => item.name)
- .sort(),
- );
- }}
- filterOption={(input, option) =>
- option?.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
- }
- >
- {typeList.map((item) => (
- <Select.Option value={item}>{item}</Select.Option>
- ))}
- </Select>
- <Card size="small" title={pluginCategory} style={{ height: 'unset' }}>
- <div
- style={{
- overflowY: 'scroll',
- height: '500px',
- display: 'flex',
- flexWrap: 'wrap',
- alignContent: 'flex-start',
- }}
- >
- {showList &&
- showList.map((name) => {
- return (
- <SidebarItem
- key={name}
- type={name}
- ports={PLUGINS_PORTS}
- properties={{
- customData: {
- type: PanelType.Plugin,
- name,
- },
- }}
- />
- );
- })}
- </div>
- </Card>
- </SSidebar>
- );
- };
- return (
- <Page>
- <SContent>
- <FlowChart
- chart={chart}
- callbacks={stateActionCallbacks}
- config={{
- zoom: { wheel: { disabled: true } },
- readonly,
- }}
- Components={{
- Port: PortCustom,
- NodeInner: NodeInnerCustom,
- }}
- />
- </SContent>
- {Boolean(!readonly) && <SSidebar>{renderSidebar()}</SSidebar>}
- </Page>
- );
-};
-
-export default SelectedSidebar;
diff --git a/web/src/components/PluginOrchestration/locales/en-US.ts b/web/src/components/PluginOrchestration/locales/en-US.ts
deleted file mode 100644
index 08ba06a..0000000
--- a/web/src/components/PluginOrchestration/locales/en-US.ts
+++ /dev/null
@@ -1,30 +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.
- */
-export default {
- 'page.siderBar.form.label.panelType.condition': 'Condition',
- 'page.siderBar.form.rule.panelType.condition': 'Please enter the condition of judgment',
- 'page.siderBar.form.label.panelType.plugin': 'Plugin Category',
-
- 'page.siderBar.button.submit': 'Save',
- 'page.siderBar.plugin': 'Plugin',
- 'page.siderBar.tips': 'Drag the required components to the panel',
-
- 'page.panel.condition.tips': 'Click here to configure',
- 'page.panel.condition.name': 'Condition',
- 'page.panel.plugin.tips': 'Click to configure the plugin',
- 'page.panel.plugin.name': 'Plugin Name',
-};
diff --git a/web/src/components/PluginOrchestration/locales/zh-CN.ts b/web/src/components/PluginOrchestration/locales/zh-CN.ts
deleted file mode 100644
index 1f1cbe3..0000000
--- a/web/src/components/PluginOrchestration/locales/zh-CN.ts
+++ /dev/null
@@ -1,30 +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.
- */
-export default {
- 'page.siderBar.form.label.panelType.condition': '判断条件',
- 'page.siderBar.form.rule.panelType.condition': '请输入判断条件',
- 'page.siderBar.form.label.panelType.plugin': '插件分类',
-
- 'page.siderBar.button.submit': '保存',
- 'page.siderBar.plugin': '插件',
- 'page.siderBar.tips': '拖动所需组件至面板',
-
- 'page.panel.condition.tips': '点击配置判断条件',
- 'page.panel.condition.name': '判断条件',
- 'page.panel.plugin.tips': '点击配置插件',
- 'page.panel.plugin.name': '插件名称',
-};
diff --git a/web/src/components/PluginOrchestration/service.ts b/web/src/components/PluginOrchestration/service.ts
deleted file mode 100644
index 7c008ff..0000000
--- a/web/src/components/PluginOrchestration/service.ts
+++ /dev/null
@@ -1,25 +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.
- */
-import { request } from 'umi';
-
-import type { PluginOrchestrationModule } from './typing';
-
-export const fetchList = () => {
- return request<Res<PluginOrchestrationModule.Meta[]>>('/plugins?all=true').then((data) => {
- return data.data;
- });
-};
diff --git a/web/src/components/PluginOrchestration/transform.ts b/web/src/components/PluginOrchestration/transform.ts
deleted file mode 100644
index 658d009..0000000
--- a/web/src/components/PluginOrchestration/transform.ts
+++ /dev/null
@@ -1,122 +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.
- */
-import { PanelType } from '.';
-
-export const transformer = (chart: any) => {
- const rule: any = {};
- const conf: any = {};
-
- const { links } = chart;
-
- const findStartNode = () => {
- const nodeIdFormArr: string[] = [];
- const nodeIdToArr: string[] = [];
- Object.keys(links).forEach((key) => {
- const item = links[key];
- nodeIdFormArr.push(item.from.nodeId);
- nodeIdToArr.push(item.to.nodeId);
- });
- return nodeIdFormArr.filter((item) => !nodeIdToArr.includes(item))[0];
- };
-
- const findLinkId = (type: string, nodeId: string, port?: string) => {
- let returnId;
-
- Object.keys(links).forEach((key) => {
- const item = links[key];
- // condition
- if (port) {
- if (port === item[type].portId && item[type].nodeId === nodeId) {
- returnId = key;
- }
- return;
- }
- // plugin
- if (nodeId === item[type].nodeId) {
- returnId = key;
- }
- });
-
- return returnId;
- };
-
- const processRule = (id: string) => {
- if (!chart.nodes[id]) return;
-
- const link = findLinkId('from', id);
- if (!link) return;
-
- const nextNodeId = links[link].to.nodeId;
-
- const nextNodeType = chart.nodes[nextNodeId].properties.customData.type;
-
- if (nextNodeType === PanelType.Plugin) {
- rule[id] = [['', nextNodeId]];
- processRule(nextNodeId);
- }
-
- if (nextNodeType === PanelType.Condition) {
- let truePortId;
- let falsePortId;
- const { ports } = chart.nodes[nextNodeId];
- Object.keys(ports).forEach((key) => {
- const item = ports[key];
- if (item.properties) {
- if (item.properties.value === 'yes') {
- truePortId = item.id;
- }
- if (item.properties.value === 'no') {
- falsePortId = item.id;
- }
- }
- });
- const trueLinkId = findLinkId('from', nextNodeId, truePortId);
- const falseLinkId = findLinkId('from', nextNodeId, falsePortId);
- const nextTrueNode = trueLinkId ? links[trueLinkId].to.nodeId : undefined;
- const nextFalseNode = falseLinkId ? links[falseLinkId].to.nodeId : undefined;
-
- rule[id] = [];
- if (nextTrueNode) {
- rule[id][0] = [chart.nodes[nextNodeId].properties.customData.name, nextTrueNode];
- processRule(nextTrueNode);
- }
-
- if (nextFalseNode) {
- rule[id][1] = ['', nextFalseNode];
- processRule(nextFalseNode);
- }
- }
- };
-
- const startId = findStartNode();
- rule.root = startId;
-
- processRule(startId);
-
- // handle conf
- Object.keys(chart.nodes).forEach((key) => {
- const item = chart.nodes[key];
- if (item.properties.customData && item.properties.customData.type === 0) {
- conf[key] = {
- name: item.properties.customData.name,
- conf: item.properties.customData.data,
- };
- }
- });
-
- return { rule, conf };
-};
diff --git a/web/src/locales/en-US.ts b/web/src/locales/en-US.ts
index e4773a6..049f03d 100644
--- a/web/src/locales/en-US.ts
+++ b/web/src/locales/en-US.ts
@@ -23,8 +23,8 @@ import pwa from './en-US/pwa';
import settingDrawer from './en-US/settingDrawer';
import settings from './en-US/setting';
import other from './en-US/other'
-import PluginOrchestration from '../components/PluginOrchestration/locales/en-US';
import Plugin from '../components/Plugin/locales/en-US';
+import PluginFlow from '../components/PluginFlow/locales/en-US';
import RawDataEditor from '../components/RawDataEditor/locales/en-US';
import UpstreamComponent from '../components/Upstream/locales/en-US'
@@ -42,8 +42,8 @@ export default {
...component,
...other,
...ActionBarEnUS,
- ...PluginOrchestration,
...Plugin,
+ ...PluginFlow,
...RawDataEditor,
...UpstreamComponent
};
diff --git a/web/src/locales/zh-CN.ts b/web/src/locales/zh-CN.ts
index 4d89580..1748751 100644
--- a/web/src/locales/zh-CN.ts
+++ b/web/src/locales/zh-CN.ts
@@ -23,8 +23,8 @@ import pwa from './zh-CN/pwa';
import other from './zh-CN/other'
import settingDrawer from './zh-CN/settingDrawer';
import settings from './zh-CN/setting';
-import PluginOrchestration from '../components/PluginOrchestration/locales/zh-CN';
import Plugin from '../components/Plugin/locales/zh-CN';
+import PluginFlow from '../components/PluginFlow/locales/zh-CN';
import RawDataEditor from '../components/RawDataEditor/locales/zh-CN';
import UpstreamComponent from '../components/Upstream/locales/zh-CN'
@@ -42,8 +42,8 @@ export default {
...component,
...other,
...ActionBarZhCN,
- ...PluginOrchestration,
...Plugin,
+ ...PluginFlow,
...RawDataEditor,
...UpstreamComponent
};
diff --git a/web/src/locales/zh-CN/component.ts b/web/src/locales/zh-CN/component.ts
index f3f813b..2acc640 100644
--- a/web/src/locales/zh-CN/component.ts
+++ b/web/src/locales/zh-CN/component.ts
@@ -76,5 +76,5 @@ export default {
'component.global.noConfigurationRequired': '无需配置',
'component.global.copy': '复制',
'component.global.copySuccess': '复制成功',
- 'component.global.copyFail': '复制失败'
+ 'component.global.copyFail': '复制失败',
};
diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx
index 295048e..770cc09 100644
--- a/web/src/pages/Route/Create.tsx
+++ b/web/src/pages/Route/Create.tsx
@@ -15,21 +15,21 @@
* limitations under the License.
*/
import React, { useState, useEffect, useRef } from 'react';
-import { Card, Steps, Form } from 'antd';
+import { Card, Steps, Form, Modal } from 'antd';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import { history, useIntl } from 'umi';
import { isEmpty } from 'lodash';
import ActionBar from '@/components/ActionBar';
+import FlowGraph from '@/components/PluginFlow/components/FlowGraph';
-import { transformer as chartTransformer } from '@/components/PluginOrchestration';
import { create, fetchItem, update, checkUniqueName, checkHostWithSSL } from './service';
import { transformProxyRewrite2Plugin } from './transform';
import Step1 from './components/Step1';
import Step2 from './components/Step2';
import Step3 from './components/Step3';
import CreateStep4 from './components/CreateStep4';
-import { DEFAULT_STEP_1_DATA, DEFAULT_STEP_3_DATA, INIT_CHART } from './constants';
+import { DEFAULT_STEP_1_DATA, DEFAULT_STEP_3_DATA } from './constants';
import ResultView from './components/ResultView';
import styles from './Create.less';
@@ -68,7 +68,6 @@ const Page: React.FC<Props> = (props) => {
const [step, setStep] = useState(1);
const [stepHeader, setStepHeader] = useState(STEP_HEADER_4);
- const [chart, setChart] = useState(INIT_CHART);
const setupRoute = (rid: number) =>
fetchItem(rid).then((data) => {
@@ -159,9 +158,8 @@ const Page: React.FC<Props> = (props) => {
data={step3Data}
isForceHttps={form1.getFieldValue('redirectOption') === 'forceHttps'}
isProxyEnable={getProxyRewriteEnable()}
- onChange={({ plugins, script = INIT_CHART, plugin_config_id }) => {
+ onChange={({ plugins, script = {}, plugin_config_id }) => {
setStep3Data({ plugins, script, plugin_config_id });
- setChart(script);
}}
/>
);
@@ -195,6 +193,39 @@ const Page: React.FC<Props> = (props) => {
return null;
};
+ const savePlugins = (): boolean => {
+ const isScriptConfigured = FlowGraph.graph?.toJSON().cells.length
+ const isPluginsConfigured = Object.keys(step3Data.plugins || {}).length
+
+ if (step === 3 && isScriptConfigured && isPluginsConfigured) {
+ Modal.confirm({
+ title: formatMessage({ id: 'component.plugin-flow.text.both-modes-exist.title' }),
+ content: formatMessage({ id: 'component.plugin-flow.text.both-modes-exist' }),
+ onOk: () => {
+ const data = FlowGraph.convertToData()
+ if (data) {
+ setStep3Data({ script: data, plugins: {} });
+ setStep(4)
+ }
+ },
+ okText: formatMessage({ id: 'component.global.confirm' }),
+ cancelText: formatMessage({ id: 'component.global.cancel' }),
+ })
+ return false
+ }
+
+ if (isScriptConfigured) {
+ const data = FlowGraph.convertToData()
+ if (!data) {
+ return false
+ }
+ setStep3Data({ script: data, plugins: {} });
+ } else {
+ setStep3Data({ ...step3Data, script: {} });
+ }
+ return true
+ }
+
const onStepChange = (nextStep: number) => {
const onUpdateOrCreate = () => {
const routeData = {
@@ -214,15 +245,6 @@ const Page: React.FC<Props> = (props) => {
}
};
- const savePlugins = () => {
- if (Object.keys(chart.nodes || {}).length) {
- const transformChart = chartTransformer(chart);
- setStep3Data({ script: { ...transformChart, chart }, plugins: {} });
- } else {
- setStep3Data({ ...step3Data, script: {} });
- }
- };
-
if (nextStep === 1) {
setStep(nextStep);
}
@@ -242,8 +264,7 @@ const Page: React.FC<Props> = (props) => {
});
});
} else {
- savePlugins();
- setStep(nextStep);
+ setStep(nextStep)
}
return;
}
@@ -260,7 +281,10 @@ const Page: React.FC<Props> = (props) => {
}
if (nextStep === 4) {
- savePlugins();
+ const result = savePlugins()
+ if (!result) {
+ return
+ }
setStep(nextStep);
}
@@ -282,7 +306,6 @@ const Page: React.FC<Props> = (props) => {
))}
</Steps>
{renderStepList()}
- {/* NOTE: PluginOrchestration works unexpected when using <renderStepList/> */}
</Card>
</PageHeaderWrapper>
<ActionBar step={step} lastStep={redirect ? 2 : 4} onChange={onStepChange} withResultView />
diff --git a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
index 26976eb..fb49fbd 100644
--- a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
+++ b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
@@ -18,8 +18,8 @@ import React from 'react';
import type { FormInstance } from 'antd/lib/form';
import { useIntl } from 'umi';
-import PluginOrchestration from '@/components/PluginOrchestration';
import PluginPage from '@/components/Plugin';
+import PluginFlow from '@/components/PluginFlow';
import Step1 from '../Step1';
import Step2 from '../Step2';
@@ -39,7 +39,7 @@ const style = {
const CreateStep4: React.FC<Props> = ({ form1, form2, redirect, upstreamRef, ...rest }) => {
const { formatMessage } = useIntl();
- const { plugins = {}, script = {}, plugin_config_id = '' } = rest.step3Data;
+ const { plugins = {}, plugin_config_id = '', script = {} } = rest.step3Data;
return (
<>
@@ -59,9 +59,9 @@ const CreateStep4: React.FC<Props> = ({ form1, form2, redirect, upstreamRef, ...
<h2 style={style}>
{formatMessage({ id: 'component.global.steps.stepTitle.pluginConfig' })}
</h2>
- {Boolean(Object.keys(plugins).length !== 0 || plugin_config_id !== '') && <PluginPage referPage = 'route' initialData={plugins} plugin_config_id={plugin_config_id} showSelector readonly />}
- {Boolean(Object.keys(script).length !== 0) && (
- <PluginOrchestration data={rest.step3Data.script.chart} readonly onChange={() => { }} />
+ {Boolean(Object.keys(plugins).length !== 0 || plugin_config_id !== '') && <PluginPage referPage='route' initialData={plugins} plugin_config_id={plugin_config_id} showSelector readonly />}
+ {Boolean(Object.keys(script || {}).length !== 0) && (
+ <PluginFlow chart={script.chart} readonly />
)}
</>
)}
diff --git a/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx b/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
index a152222..fa4b840 100644
--- a/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
+++ b/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
@@ -112,9 +112,9 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
}
}
}
- case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput]:{
+ case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput]: {
let contentType = [''];
- switch (bodyCodeMirrorMode){
+ switch (bodyCodeMirrorMode) {
case DEBUG_BODY_CODEMIRROR_MODE_SUPPORTED[0].mode:
contentType = ['application/json;charset=UTF-8'];
break;
@@ -209,7 +209,7 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
const handleDebug = (url: string) => {
/* eslint-disable no-useless-escape */
- if (!urlRegexSafe({exact: true, strict: false}).test(url)) {
+ if (!urlRegexSafe({ exact: true, strict: false }).test(url)) {
notification.warning({
message: formatMessage({ id: 'page.route.input.placeholder.requestUrl' }),
});
@@ -217,7 +217,7 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
}
const queryFormData = transformHeaderAndQueryParamsFormData(queryForm.getFieldsValue().params);
const bodyFormRelateData = transformBodyParamsFormData();
- const {bodyFormData, header: bodyFormHeader} = bodyFormRelateData;
+ const { bodyFormData, header: bodyFormHeader } = bodyFormRelateData;
const pureHeaderFormData = transformHeaderAndQueryParamsFormData(
headerForm.getFieldsValue().params,
);
@@ -234,12 +234,12 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
}, bodyFormData)
.then((req) => {
setLoading(false);
- const resp: RouteModule.debugResponse= req.data;
+ const resp: RouteModule.debugResponse = req.data;
if (typeof (resp.data) !== 'string') {
resp.data = JSON.stringify(resp.data, null, 2);
}
setResponse(resp);
- const contentType=resp.header["Content-Type"];
+ const contentType = resp.header["Content-Type"];
if (contentType == null || contentType.length !== 1) {
setResponseBodyCodeMirrorMode("TEXT");
} else if (contentType[0].toLowerCase().indexOf("json") !== -1) {
@@ -331,13 +331,13 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
>
<Tabs>
<TabPane data-cy='query' tab={formatMessage({ id: 'page.route.TabPane.queryParams' })} key="query">
- <DebugParamsView form={queryForm} name='queryForm'/>
+ <DebugParamsView form={queryForm} name='queryForm' />
</TabPane>
<TabPane data-cy='auth' tab={formatMessage({ id: 'page.route.TabPane.authentication' })} key="auth">
<AuthenticationView form={authForm} />
</TabPane>
<TabPane data-cy='header' tab={formatMessage({ id: 'page.route.TabPane.headerParams' })} key="header">
- <DebugParamsView form={headerForm} name='headerForm' inputType="header"/>
+ <DebugParamsView form={headerForm} name='headerForm' inputType="header" />
</TabPane>
{showBodyTab && (
<TabPane data-cy='body' tab={formatMessage({ id: 'page.route.TabPane.bodyParams' })} key="body">
@@ -371,7 +371,7 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
)}
<div style={{ marginTop: 16 }}>
{bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormUrlencoded] && (
- <DebugParamsView form={urlencodedForm} name='urlencodedForm'/>
+ <DebugParamsView form={urlencodedForm} name='urlencodedForm' />
)}
{bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormData] && (
@@ -420,7 +420,7 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
onSelect={(mode) => setResponseBodyCodeMirrorMode(mode as string)}>
{
DEBUG_RESPONSE_BODY_CODEMIRROR_MODE_SUPPORTED.map(mode => {
- return <Option value={mode.mode}>{mode.name}</Option>
+ return <Option value={mode.mode} key={mode.mode}>{mode.name}</Option>
})
}
</Select>
@@ -438,10 +438,10 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
});
}}>
<Button type="text" disabled={!response}>
- <CopyOutlined/>
+ <CopyOutlined />
</Button>
</CopyToClipboard>
- <div id='codeMirror-response' style={{marginTop:16}}>
+ <div id='codeMirror-response' style={{ marginTop: 16 }}>
<CodeMirror
value={response ? response.data : ""}
height={codeMirrorHeight}
diff --git a/web/src/pages/Route/components/Step3/index.tsx b/web/src/pages/Route/components/Step3/index.tsx
index 614ae53..2977892 100644
--- a/web/src/pages/Route/components/Step3/index.tsx
+++ b/web/src/pages/Route/components/Step3/index.tsx
@@ -20,13 +20,19 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
import { isChrome, isChromium, isEdgeChromium } from 'react-device-detect';
import { useIntl } from 'umi';
-import PluginOrchestration from '@/components/PluginOrchestration';
import PluginPage from '@/components/Plugin';
+import PluginFlow from '@/components/PluginFlow';
+import { DEFAULT_PLUGIN_FLOW_DATA } from '@/components/PluginFlow/constants';
+import FlowGraph from '@/components/PluginFlow/components/FlowGraph';
type Props = {
data: {
plugins: PluginComponent.Data;
- script: Record<string, any>;
+ script: {
+ chart: Record<string, any>;
+ rule: Record<string, any>;
+ conf: Record<string, any>;
+ };
plugin_config_id?: string;
};
onChange: (data: { plugins: PluginComponent.Data; script: any, plugin_config_id?: string; }) => void;
@@ -39,15 +45,13 @@ type Mode = 'NORMAL' | 'DRAW';
const Page: React.FC<Props> = ({ data, onChange, readonly = false, isForceHttps = false, isProxyEnable = false }) => {
const { formatMessage } = useIntl();
- const { plugins = {}, script = {}, plugin_config_id = '' } = data;
+ const { plugins = {}, script = DEFAULT_PLUGIN_FLOW_DATA, plugin_config_id = '' } = data;
// NOTE: Currently only compatible with chrome
const useSupportBrowser = isChrome || isEdgeChromium || isChromium;
const disableDraw = !useSupportBrowser || isForceHttps || isProxyEnable;
- const type = Object.keys(script || {}).length === 0 || disableDraw ? 'NORMAL' : 'DRAW';
-
- const [mode, setMode] = useState<Mode>(type);
+ const [mode, setMode] = useState<Mode>(Object.keys(script.chart?.cells || {}).length === 0 || disableDraw ? 'NORMAL' : 'DRAW');
return (
<>
@@ -55,15 +59,20 @@ const Page: React.FC<Props> = ({ data, onChange, readonly = false, isForceHttps
<Radio.Group
value={mode}
onChange={(e) => {
+ if (e.target.value === 'NORMAL') {
+ // NOTE: current is DRAW
+ onChange({ ...data, script: { chart: FlowGraph.graph.toJSON() } })
+ }
+
setMode(e.target.value);
}}
style={{ marginBottom: 10 }}
>
<Radio.Button value="NORMAL">
- { formatMessage({ id: 'page.route.tabs.normalMode' }) }
+ {formatMessage({ id: 'page.route.tabs.normalMode' })}
</Radio.Button>
<Radio.Button value="DRAW" disabled={disableDraw}>
- { formatMessage({ id: 'page.route.tabs.orchestration' }) }
+ {formatMessage({ id: 'page.route.tabs.orchestration' })}
</Radio.Button>
</Radio.Group>
{Boolean(disableDraw) && (
@@ -74,13 +83,13 @@ const Page: React.FC<Props> = ({ data, onChange, readonly = false, isForceHttps
// NOTE: forceHttps do not support DRAW mode
const titleArr: string[] = [];
if (!useSupportBrowser) {
- titleArr.push(formatMessage({id: 'page.route.tooltip.pluginOrchOnlySuportChrome'}));
+ titleArr.push(formatMessage({ id: 'page.route.tooltip.pluginOrchOnlySuportChrome' }));
}
if (isForceHttps) {
- titleArr.push(formatMessage({id: 'page.route.tooltip.pluginOrchWithoutRedirect'}));
+ titleArr.push(formatMessage({ id: 'page.route.tooltip.pluginOrchWithoutRedirect' }));
}
if (isProxyEnable) {
- titleArr.push(formatMessage({id: 'page.route.tooltip.pluginOrchWithoutProxyRewrite'}));
+ titleArr.push(formatMessage({ id: 'page.route.tooltip.pluginOrchWithoutProxyRewrite' }));
}
return titleArr.map((item, index) => `${index + 1}.${item}`).join('');
}}
@@ -92,23 +101,18 @@ const Page: React.FC<Props> = ({ data, onChange, readonly = false, isForceHttps
</div>
{Boolean(mode === 'NORMAL') && (
<PluginPage
+ readonly={readonly}
initialData={plugins}
plugin_config_id={plugin_config_id}
schemaType="route"
referPage="route"
showSelector
onChange={(pluginsData, id) => {
- onChange({ plugins: pluginsData, script: {}, plugin_config_id: id })
+ onChange({ ...data, plugins: pluginsData, plugin_config_id: id })
}}
/>
)}
- {Boolean(mode === 'DRAW') && (
- <PluginOrchestration
- data={script?.chart}
- onChange={(scriptData) => onChange({ plugins: {}, script: scriptData })}
- readonly={readonly}
- />
- )}
+ {Boolean(mode === 'DRAW') && (<PluginFlow chart={script.chart as any} />)}
</>
);
};
diff --git a/web/src/pages/Route/constants.ts b/web/src/pages/Route/constants.ts
index f362736..f0aeea9 100644
--- a/web/src/pages/Route/constants.ts
+++ b/web/src/pages/Route/constants.ts
@@ -76,15 +76,6 @@ export const DEFAULT_STEP_3_DATA: RouteModule.Step3Data = {
plugin_config_id: ""
};
-export const INIT_CHART = {
- offset: { x: 55.71, y: 21.69 },
- scale: 0.329,
- nodes: {},
- links: {},
- selected: {},
- hovered: {},
-};
-
export const AUTH_LIST = ['basic-auth', 'jwt-auth', 'key-auth'];
export const HEADER_LIST = [
diff --git a/web/src/pages/Route/locales/en-US.ts b/web/src/pages/Route/locales/en-US.ts
index 2609800..60bc6b9 100644
--- a/web/src/pages/Route/locales/en-US.ts
+++ b/web/src/pages/Route/locales/en-US.ts
@@ -146,8 +146,8 @@ export default {
'page.route.tooltip.pluginOrchWithoutProxyRewrite': 'Plugin orchestration mode cannot be used when request override is configured in Step 1.',
'page.route.tooltip.pluginOrchWithoutRedirect': 'Plugin orchestration mode cannot be used when Redirect in Step 1 is selected to enable HTTPS.',
- 'page.route.tabs.normalMode': 'Normal mode',
- 'page.route.tabs.orchestration': 'Plugin orchestration',
+ 'page.route.tabs.normalMode': 'Normal',
+ 'page.route.tabs.orchestration': 'Orchestration',
'page.route.list.description': 'Route is the entry point of a request, which defines the matching rules between a client request and a service. A route can be associated with a service (Service), an upstream (Upstream), a service can correspond to a set of routes, and a route can correspond to an upstream object (a set of backend service nodes), so each request matching to a route will be proxied by the gateway to the route-bound upstream service.',
diff --git a/web/src/pages/Route/locales/zh-CN.ts b/web/src/pages/Route/locales/zh-CN.ts
index 3c134bc..80a82bf 100644
--- a/web/src/pages/Route/locales/zh-CN.ts
+++ b/web/src/pages/Route/locales/zh-CN.ts
@@ -146,7 +146,7 @@ export default {
'page.route.tooltip.pluginOrchWithoutRedirect': '当步骤一中 重定向 选择为 启用 HTTPS 时,不可使用插件编排模式。',
'page.route.tabs.normalMode': '普通模式',
- 'page.route.tabs.orchestration': '插件编排',
+ 'page.route.tabs.orchestration': '编排模式',
'page.route.list.description': '路由(Route)是请求的入口点,它定义了客户端请求与服务之间的匹配规则。路由可以与服务(Service)、上游(Upstream)关联,一个服务可对应一组路由,一个路由可以对应一个上游对象(一组后端服务节点),因此,每个匹配到路由的请求将被网关代理到路由绑定的上游服务中。',
diff --git a/web/src/pages/Service/locales/en-US.ts b/web/src/pages/Service/locales/en-US.ts
index ca4952e..2c0f2e9 100644
--- a/web/src/pages/Service/locales/en-US.ts
+++ b/web/src/pages/Service/locales/en-US.ts
@@ -15,8 +15,8 @@
* limitations under the License.
*/
export default {
- 'page.service.steps.stepTitle.basicInformation': 'Basic Information',
- 'page.service.steps.stepTitle.pluginConfig': 'Plugin Config',
+ 'page.service.steps.stepTitle.basicInformation': 'Basic',
+ 'page.service.steps.stepTitle.pluginConfig': 'Plugin',
'page.service.steps.stepTitle.preview': 'Preview',
'page.service.list': 'Service List',
'page.service.description': 'A service consists of a combination of public plugin configuration and upstream target information in a route. Services are associated with Routes and Upstreams, and a service can correspond to a set of upstream nodes and can be bound by multiple routes.',
diff --git a/web/yarn.lock b/web/yarn.lock
index b0abedb..446e9c2 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -2,6 +2,11 @@
# yarn lockfile v1
+"@4tw/cypress-drag-drop@^1.6.0":
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/@4tw/cypress-drag-drop/-/cypress-drag-drop-1.6.0.tgz#528cf837f16d16e059b2e3cae702d9de2c9e1088"
+ integrity sha512-B61iPspk2hZuuo3mjmlTqYZXJ9tusc8VyEk+5KMO/FTBrHKDWqYp8ANOJnIkRz6QfYZbx+qBoKBu7MTfvBCKew==
+
"@ahooksjs/use-request@^2.0.0":
version "2.8.3"
resolved "https://registry.yarnpkg.com/@ahooksjs/use-request/-/use-request-2.8.3.tgz#9b7eff972658497473f61ceb89a268d665e28aeb"
@@ -185,6 +190,31 @@
lodash "^4.17.15"
resize-observer-polyfill "^1.5.0"
+"@antv/x6-react-components@^1.1.7":
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/@antv/x6-react-components/-/x6-react-components-1.1.7.tgz#4f7d017b05dd3adb767a359bb0b9b4566b84ef8f"
+ integrity sha512-2G+J6GmCy6wfHJeDpopC7px6XbFc9zy0EFKAHkgOUS5XVj/n91VzvKViHd8miBRudkZ0ZTkf9NxtIR19hdOq6w==
+ dependencies:
+ clamp "^1.0.1"
+ classnames "^2.2.6"
+ rc-dropdown "^3.0.0-alpha.0"
+ rc-util "^4.15.7"
+ react-color "^2.17.3"
+ react-resize-detector "^6.6.4"
+ ua-parser-js "^0.7.20"
+
+"@antv/x6@^1.18.5":
+ version "1.18.5"
+ resolved "https://registry.yarnpkg.com/@antv/x6/-/x6-1.18.5.tgz#0215e8abbebd2ee508943aa58e62a88e2760799a"
+ integrity sha512-Bxn1pl5etaiDmO61Pc8EaqPqjld2vRz4+wF55VqFsGy2SpVYS1mPYZury1v5qYzAsU/BtQCW0j0RHppPR4U+UA==
+ dependencies:
+ csstype "^3.0.3"
+ jquery "^3.5.1"
+ jquery-mousewheel "^3.1.13"
+ lodash-es "^4.17.15"
+ mousetrap "^1.6.5"
+ utility-types "^3.10.0"
+
"@babel/code-frame@7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
@@ -1879,6 +1909,11 @@
dependencies:
"@hapi/hoek" "^9.0.0"
+"@icons/material@^0.2.4":
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
+ integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
+
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -2158,17 +2193,6 @@
dependencies:
extend "3.0.2"
-"@mrblenny/react-flow-chart@^0.0.14":
- version "0.0.14"
- resolved "https://registry.yarnpkg.com/@mrblenny/react-flow-chart/-/react-flow-chart-0.0.14.tgz#be11d06345c7222b41f488b38011b109e48a04b3"
- integrity sha512-3bFjlmlYuqHpCRCPoA59jok2Vhe59ZKT5g9lb6U5IM+Zk2fIsKmXp8LEcliW0TrHtNMtZw5Gm3/rScrg/DwAFQ==
- dependencies:
- pathfinding "^0.4.18"
- react-draggable "^4.4.3"
- react-resize-observer "^1.1.1"
- react-zoom-pan-pinch "^1.6.1"
- uuid "^3.3.2"
-
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -2899,6 +2923,11 @@
"@types/scheduler" "*"
csstype "^3.0.2"
+"@types/resize-observer-browser@^0.1.5":
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz#36d897708172ac2380cd486da7a3daf1161c1e23"
+ integrity sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ==
+
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -5489,6 +5518,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
inherits "^2.0.1"
safe-buffer "^5.0.1"
+clamp@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/clamp/-/clamp-1.0.1.tgz#66a0e64011816e37196828fdc8c8c147312c8634"
+ integrity sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ=
+
class-utils@^0.3.5:
version "0.3.6"
resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
@@ -6352,6 +6386,11 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b"
integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==
+csstype@^3.0.3:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
+ integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
+
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@@ -8757,11 +8796,6 @@ hasha@^5.0.0:
is-stream "^2.0.0"
type-fest "^0.8.0"
-heap@0.2.5:
- version "0.2.5"
- resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.5.tgz#713b65590ebcc40fcbeeaf55e851694092b39af1"
- integrity sha1-cTtlWQ68xA/L7q9V6FFpQJKzmvE=
-
history-with-query@4.10.4:
version "4.10.4"
resolved "https://registry.yarnpkg.com/history-with-query/-/history-with-query-4.10.4.tgz#8161ff3c5044e29dfaeb73e7587eb3d4c1a8090e"
@@ -10326,6 +10360,16 @@ joi@^17.3.0:
"@sideway/formula" "^3.0.0"
"@sideway/pinpoint" "^2.0.0"
+jquery-mousewheel@^3.1.13:
+ version "3.1.13"
+ resolved "https://registry.yarnpkg.com/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz#06f0335f16e353a695e7206bf50503cb523a6ee5"
+ integrity sha1-BvAzXxbjU6aV5yBr9QUDy1I6buU=
+
+jquery@^3.5.1:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470"
+ integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==
+
js-beautify@^1.13.0:
version "1.13.5"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.13.5.tgz#a08a97890cae55daf1d758d3f6577bd4a64d7014"
@@ -10868,6 +10912,11 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
+lodash-es@^4.17.15:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+ integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -10968,7 +11017,7 @@ lodash@4.17.20:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
-lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
+lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -11168,6 +11217,11 @@ marked@1.2.7:
resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.7.tgz#6e14b595581d2319cdcf033a24caaf41455a01fb"
integrity sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA==
+material-colors@^1.2.1:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
+ integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
+
mathml-tag-names@^2.0.1, mathml-tag-names@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
@@ -11579,6 +11633,11 @@ moo@^0.5.0:
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==
+mousetrap@^1.6.5:
+ version "1.6.5"
+ resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
+ integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -12565,13 +12624,6 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-pathfinding@^0.4.18:
- version "0.4.18"
- resolved "https://registry.yarnpkg.com/pathfinding/-/pathfinding-0.4.18.tgz#a9990f6fa22b7ef196e5651b049165403a045fe8"
- integrity sha1-qZkPb6IrfvGW5WUbBJFlQDoEX+g=
- dependencies:
- heap "0.2.5"
-
pause-stream@0.0.11:
version "0.0.11"
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
@@ -13348,7 +13400,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
-prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -13691,7 +13743,7 @@ rc-drawer@~4.3.0:
classnames "^2.2.6"
rc-util "^5.7.0"
-rc-dropdown@^3.1.3, rc-dropdown@~3.2.0:
+rc-dropdown@^3.0.0-alpha.0, rc-dropdown@^3.1.3, rc-dropdown@~3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/rc-dropdown/-/rc-dropdown-3.2.0.tgz#da6c2ada403842baee3a9e909a0b1a91ba3e1090"
integrity sha512-j1HSw+/QqlhxyTEF6BArVZnTmezw2LnSmRk6I9W7BCqNCKaRwleRmMMs1PHbuaG8dKHVqP6e21RQ7vPBLVnnNw==
@@ -14018,7 +14070,7 @@ rc-upload@~4.2.0-alpha.0:
classnames "^2.2.5"
rc-util "^5.2.0"
-rc-util@4.x, rc-util@^4.0.4, rc-util@^4.15.3, rc-util@^4.4.0:
+rc-util@4.x, rc-util@^4.0.4, rc-util@^4.15.3, rc-util@^4.15.7, rc-util@^4.4.0:
version "4.21.1"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.21.1.tgz#88602d0c3185020aa1053d9a1e70eac161becb05"
integrity sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==
@@ -14068,6 +14120,19 @@ react-app-polyfill@^1.0.4:
regenerator-runtime "^0.13.3"
whatwg-fetch "^3.0.0"
+react-color@^2.17.3:
+ version "2.19.3"
+ resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d"
+ integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==
+ dependencies:
+ "@icons/material" "^0.2.4"
+ lodash "^4.17.15"
+ lodash-es "^4.17.15"
+ material-colors "^1.2.1"
+ prop-types "^15.5.10"
+ reactcss "^1.2.0"
+ tinycolor2 "^1.4.1"
+
react-copy-to-clipboard@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.3.tgz#2a0623b1115a1d8c84144e9434d3342b5af41ab4"
@@ -14124,14 +14189,6 @@ react-dom@16.x, react-dom@^16.8.6:
prop-types "^15.6.2"
scheduler "^0.19.1"
-react-draggable@^4.4.3:
- version "4.4.3"
- resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.3.tgz#0727f2cae5813e36b0e4962bf11b2f9ef2b406f3"
- integrity sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==
- dependencies:
- classnames "^2.2.5"
- prop-types "^15.6.0"
-
react-error-overlay@^5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.6.tgz#0cd73407c5d141f9638ae1e0c63e7b2bf7e9929d"
@@ -14221,10 +14278,15 @@ react-refresh@0.9.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==
-react-resize-observer@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/react-resize-observer/-/react-resize-observer-1.1.1.tgz#641dfa2e0f4bd2549a8ab4bbbaf43b68f3dcaf76"
- integrity sha512-3R+90Hou90Mr3wJYc+unsySC8Pn91V4nmjO32NKvUvjphRUbq9HisyLg7bDyGBE7xlMrrM6Fax7iNQaFdc/FYA==
+react-resize-detector@^6.6.4:
+ version "6.6.5"
+ resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-6.6.5.tgz#adde70db9c76da09892134b8f6c4dfd351cdd93c"
+ integrity sha512-khKS1IpC2cfx5+6G9HkAU/9CGjDV8woE57pVeH8nP5Ji52yXz6MpQEHEzJZ2obGghWrewN4php8ArxB4yWNqZA==
+ dependencies:
+ "@types/resize-observer-browser" "^0.1.5"
+ lodash.debounce "^4.0.8"
+ lodash.throttle "^4.1.1"
+ resize-observer-polyfill "^1.5.1"
react-router-config@5.1.1:
version "5.1.1"
@@ -14275,11 +14337,6 @@ react-tween-state@^0.1.5:
raf "^3.1.0"
tween-functions "^1.0.1"
-react-zoom-pan-pinch@^1.6.1:
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-1.6.1.tgz#da16267c258ab37e8ebcdc7c252794a9633e91ec"
- integrity sha512-J2eM0gZ04XiUWvmKZrOhSAB2zjyoK7kw2POIeN1X0yTTlmp6HPGV0zYfjnlkhgt8nQwpvXAbsF/oAnkuiwk1kA==
-
react@16.x, react@^16.8.6:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
@@ -14289,6 +14346,13 @@ react@16.x, react@^16.8.6:
object-assign "^4.1.1"
prop-types "^15.6.2"
+reactcss@^1.2.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
+ integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==
+ dependencies:
+ lodash "^4.0.1"
+
read-only-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0"
@@ -16577,6 +16641,11 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3:
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+tinycolor2@^1.4.1:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
+ integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
+
tlds@^1.217.0:
version "1.218.0"
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.218.0.tgz#f31804891c650c136f88cb8ec2f043577b5f5afd"
@@ -16860,6 +16929,11 @@ ua-parser-js@^0.7.18, ua-parser-js@^0.7.24:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c"
integrity sha512-yo+miGzQx5gakzVK3QFfN0/L9uVhosXBBO7qmnk7c2iw1IhL212wfA3zbnI54B0obGwC/5NWub/iT9sReMx+Fw==
+ua-parser-js@^0.7.20:
+ version "0.7.28"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
+ integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
+
umd@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf"
@@ -17285,6 +17359,11 @@ util@~0.10.1:
dependencies:
inherits "2.0.3"
+utility-types@^3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
+ integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
+
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"