mirror of
https://github.com/apache/apisix-dashboard.git
synced 2025-12-08 20:12:51 +00:00
feat: refactor Plugin Orchestration (#1813)
This commit is contained in:
parent
059b30b3c4
commit
423c9e8996
@ -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
|
||||
|
||||
@ -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: |
|
||||
|
||||
1
LICENSE
1
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
|
||||
|
||||
========================================================================
|
||||
|
||||
21
licenses/LICENSE-antvis-x6.txt
Normal file
21
licenses/LICENSE-antvis-x6.txt
Normal file
@ -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.
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
});
|
||||
};
|
||||
|
||||
203
web/src/components/PluginFlow/PluginFlow.tsx
Normal file
203
web/src/components/PluginFlow/PluginFlow.tsx
Normal file
@ -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
|
||||
394
web/src/components/PluginFlow/components/FlowGraph/FlowGraph.ts
Normal file
394
web/src/components/PluginFlow/components/FlowGraph/FlowGraph.ts
Normal file
@ -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
|
||||
@ -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'
|
||||
161
web/src/components/PluginFlow/components/FlowGraph/shapes.ts
Normal file
161
web/src/components/PluginFlow/components/FlowGraph/shapes.ts
Normal file
@ -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',
|
||||
}
|
||||
],
|
||||
},
|
||||
})
|
||||
195
web/src/components/PluginFlow/components/Toolbar/index.tsx
Normal file
195
web/src/components/PluginFlow/components/Toolbar/index.tsx
Normal file
@ -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
|
||||
294
web/src/components/PluginFlow/constants.ts
Normal file
294
web/src/components/PluginFlow/constants.ts
Normal file
@ -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: ""
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
41
web/src/components/PluginFlow/locales/en-US.ts
Normal file
41
web/src/components/PluginFlow/locales/en-US.ts
Normal file
@ -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'
|
||||
}
|
||||
41
web/src/components/PluginFlow/locales/zh-CN.ts
Normal file
41
web/src/components/PluginFlow/locales/zh-CN.ts
Normal file
@ -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': '配置冲突'
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
`;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
@ -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;
|
||||
@ -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',
|
||||
};
|
||||
@ -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': '插件名称',
|
||||
};
|
||||
@ -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;
|
||||
});
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -76,5 +76,5 @@ export default {
|
||||
'component.global.noConfigurationRequired': '无需配置',
|
||||
'component.global.copy': '复制',
|
||||
'component.global.copySuccess': '复制成功',
|
||||
'component.global.copyFail': '复制失败'
|
||||
'component.global.copyFail': '复制失败',
|
||||
};
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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 />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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} />)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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.',
|
||||
|
||||
|
||||
@ -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)关联,一个服务可对应一组路由,一个路由可以对应一个上游对象(一组后端服务节点),因此,每个匹配到路由的请求将被网关代理到路由绑定的上游服务中。',
|
||||
|
||||
|
||||
@ -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.',
|
||||
|
||||
167
web/yarn.lock
167
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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user