feat: 数据源面板

This commit is contained in:
muyun.my 2020-09-15 13:55:35 +08:00
parent 1f03857532
commit 56eaff52c8
25 changed files with 1579 additions and 2 deletions

View File

@ -10,7 +10,21 @@
},
"fileName": "test",
"dataSource": {
"list": []
"list": [
{
"type": "http",
"id": "http1",
"isInit": true,
"options": {
"uri": "https://www.taobao.com",
"params": {
"a": 1,
"b": true,
"c": "3"
}
}
}
]
},
"state": {
"text": "outter",
@ -578,4 +592,4 @@
]
}
]
}
}

View File

@ -3,6 +3,7 @@ import samplePreview from '@ali/lowcode-plugin-sample-preview';
import undoRedo from '@ali/lowcode-plugin-undo-redo';
import componentsPane from '@ali/lowcode-plugin-components-pane';
import outline, { OutlinePane } from '@ali/lowcode-plugin-outline-pane';
import dataSourcePane from '@ali/lowcode-plugin-datasource-pane';
import zhEn from '@ali/lowcode-plugin-zh-en';
import eventBindDialog from '@ali/lowcode-plugin-event-bind-dialog';
import variableBindDialog from '@ali/lowcode-plugin-variable-bind-dialog';
@ -23,4 +24,5 @@ export default {
sourceEditor,
codeout,
saveload,
dataSourcePane,
};

View File

@ -1,3 +1,5 @@
import { DataSourceImportPluginCode } from '@ali/lowcode-plugin-datasource-pane';
export default {
plugins: {
topArea: [
@ -97,6 +99,60 @@ export default {
},
},
},
{
pluginKey: 'dataSourcePane',
pluginProps: {
importPlugins: [
{
name: 'code2',
title: '源码2',
content: DataSourceImportPluginCode,
},
],
dataSourceTypes: [
{
type: 'mopen',
schema: {
type: 'object',
properties: {
options: {
type: 'object',
properties: {
uri: {
title: 'api',
},
v: {
title: 'v',
type: 'string',
},
appKey: {
title: 'appKey',
type: 'string',
},
},
},
},
},
},
],
},
type: 'PanelIcon',
props: {
align: 'top',
icon: 'wenjian',
description: '数据源面板',
panelProps: {
floatable: true,
height: 300,
help: undefined,
hideTitleBar: false,
maxHeight: 800,
maxWidth: 1200,
title: '数据源面板',
width: 600,
},
},
},
{
pluginKey: 'zhEn',
type: 'Custom',

View File

@ -0,0 +1,71 @@
TODO
---
* 多语言
* [later]表达式和其他类型的切换
* 现有场景代码的兼容
* class publich method bind issue
* ICON
* 支持变量
## 数据源面板
数据源管理
* 新建
* 编辑
* 导入
* 指定数据源类型
* 定制数据源导入插件
## 数据源类型定义
内置变量http 和 mtop 类型,支持传入自定义类型。
```
type DataSourceType = {
type: string;
optionsSchema: JSONSchema6
};
```
数据源类型需要在集团规范约束下扩展。
目前只允许在 options 下添加扩展字段。
比如 mtop 类型,需要添加 options.v (版本)字段。
## 导入插件
默认支持源码导入,可以传入自定义插件。
```
interface DataSourcePaneImportPlugin {
name: string;
title: string;
component: React.ReactNode;
componentProps?: DataSourcePaneImportPluginCustomProps;
}
interface DataSourcePaneImportPluginCustomProps {
[customPropName: string]: any;
}
interface DataSourcePaneImportPluginComponentProps extends DataSourcePaneImportPluginCustomProps {
onChange: (dataSourceList: DataSourceConfig[]) => void;
}
```
## 问题
* 变量,上下文放数据源里管理是否合适
* mockUrl 和 mockData
* 设计器的设计语言无法统一
## 插件开发
<https://yuque.antfin-inc.com/ali-lowcode/docs/ip4awq>
## node 版本
v14.4.0

View File

@ -0,0 +1,7 @@
{
"plugins": [
[
"build-plugin-component"
]
]
}

View File

@ -0,0 +1,44 @@
{
"name": "@ali/lowcode-plugin-datasource-pane",
"version": "1.0.7-0",
"description": "低代码引擎数据源面板",
"main": "lib/index.js",
"files": [
"lib"
],
"scripts": {
"build": "tsc",
"test": "ava",
"test:snapshot": "ava --update-snapshots"
},
"ava": {
"compileEnhancements": false,
"snapshotDir": "test/fixtures/__snapshots__",
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
},
"license": "MIT",
"devDependencies": {
"@types/json-schema": "^7.0.6",
"@types/react": "^16.9.49",
"@types/traverse": "^0.6.32",
"monaco-editor": "^0.20.0"
},
"dependencies": {
"@ali/lowcode-editor-setters": "^1.0.7-0",
"@alifd/next": "^1.20.28",
"@formily/next": "^1.3.2",
"@formily/next-components": "^1.3.2",
"@formily/react-schema-renderer": "^1.3.2",
"@types/traverse": "^0.6.32",
"ajv": "^6.12.4",
"lodash": "^4.17.20",
"react-monaco-editor": "^0.40.0",
"styled-components": "^5.2.0",
"traverse": "^0.6.6"
}
}

View File

@ -0,0 +1,273 @@
// @todo schema default
import React, { PureComponent, ReactElement, FC } from 'react';
import { SchemaForm, FormButtonGroup, Submit } from '@formily/next';
import { ArrayTable, Input, Switch, NumberPicker } from '@formily/next-components';
import _isPlainObject from 'lodash/isPlainObject';
import _isArray from 'lodash/isArray';
import _isNumber from 'lodash/isNumber';
import _isString from 'lodash/isString';
import _isBoolean from 'lodash/isBoolean';
import _cloneDeep from 'lodash/cloneDeep';
import _mergeWith from 'lodash/mergeWith';
import _get from 'lodash/get';
import _tap from 'lodash/tap';
import traverse from 'traverse';
import { ParamValue, JSFunction } from './form-components';
import { DataSourceType, DataSourceConfig } from './types';
// @todo $ref
const SCHEMA = {
type: 'object',
properties: {
type: {
title: '类型',
type: 'string',
editable: false,
},
id: {
type: 'string',
title: '数据源 ID',
required: true,
},
isInit: {
title: '是否自动请求',
type: 'boolean',
default: true,
},
dataHandler: {
type: 'string',
title: '单个数据结果处理函数',
required: true,
'x-component': 'JSFunction',
default: 'function() {}',
},
options: {
type: 'object',
title: '请求参数',
required: true,
properties: {
uri: {
type: 'string',
title: '请求地址',
required: true,
},
params: {
title: '请求参数',
type: 'object',
default: {},
},
method: {
type: 'string',
title: '请求方法',
required: true,
enum: ['GET', 'POST', 'OPTIONS', 'PUT', 'DELETE'],
default: 'GET',
},
isCors: {
type: 'boolean',
title: '是否支持跨域',
required: true,
default: true,
},
timeout: {
type: 'number',
title: '超时时长(毫秒)',
default: 5000,
},
headers: {
type: 'object',
title: '请求头信息',
default: {},
},
},
},
},
};
export interface DataSourceFormProps {
dataSourceType: DataSourceType;
dataSource?: DataSourceConfig;
omComplete?: (dataSource: DataSourceConfig) => void;
}
export interface DataSourceFormState {
}
/**
* ID
*/
export class DataSourceForm extends PureComponent<DataSourceFormProps, DataSourceFormState> {
state = {};
handleFormSubmit = (formData) => {
// @todo mutable?
if (_isArray(_get(formData, 'options.params'))) {
formData.options.params = formData.options.params.reduce((acc, cur) => {
if (!cur.name) return;
acc[cur.name] = cur.value;
return acc;
}, {});
}
if (_isArray(_get(formData, 'options.headers'))) {
formData.options.headers = formData.options.headers.reduce((acc, cur) => {
if (!cur.name) return;
acc[cur.name] = cur.value;
return acc;
}, {});
}
console.log('submit', formData);
this.props?.onComplete(formData);
};
deriveInitialData = (dataSource = {}) => {
const { dataSourceType } = this.props;
const result = _cloneDeep(dataSource);
if (_isPlainObject(_get(result, 'options.params'))) {
result.options.params = Object.keys(result.options.params).reduce(
(acc, cur) => {
acc.push({
name: cur,
value: result.options.params[cur]
});
return acc;
},
[]
);
}
if (_isPlainObject(_get(result, 'options.headers'))) {
result.options.headers = Object.keys(result.options.headers).reduce(
(acc, cur) => {
acc.push({
name: cur,
value: result.options.headers[cur]
});
return acc;
},
[]
);
}
result.type = dataSourceType.type;
return result;
}
deriveSchema = () => {
const { dataSourceType } = this.props;
// @todo 减小覆盖的风险
const formSchema = _mergeWith({}, SCHEMA, dataSourceType.schema, (objValue, srcValue) => {
if (_isArray(objValue)) {
return srcValue;
}
});
debugger;
if (_get(formSchema, 'properties.options.properties.params')) {
formSchema.properties.options.properties.params = {
...formSchema.properties.options.properties.params,
type: 'array',
'x-component': 'ArrayTable',
'x-component-props': {
operationsWidth: 100,
},
items: {
type: 'object',
properties: {
name: {
title: '参数名',
type: 'string',
},
value: {
title: '参数值',
type: 'string',
'x-component': 'ParamValue',
'x-component-props': {
types: [
'string',
'boolean',
'expression',
'number'
],
},
},
},
}
};
delete formSchema.properties.options.properties.params.properties;
}
if (_get(formSchema, 'properties.options.properties.headers')) {
formSchema.properties.options.properties.headers = {
...formSchema.properties.options.properties.headers,
type: 'array',
'x-component': 'ArrayTable',
'x-component-props': {
operationsWidth: 100,
},
items: {
type: 'object',
properties: {
name: {
title: '参数名',
type: 'string',
},
value: {
title: '参数值',
type: 'string',
'x-component': 'ParamValue',
'x-component-props': {
types: [
'string'
],
},
},
},
},
};
delete formSchema.properties.options.properties.headers.properties;
}
return traverse(formSchema).forEach(function(node) {
if (node?.type && !node['x-component']) {
if (node.type === 'string') {
node['x-component'] = 'Input';
} else if (node.type === 'number') {
node['x-component'] = 'NumberPicker';
} else if (node.type === 'boolean') {
node['x-component'] = 'Switch';
}
}
});
};
render() {
const { dataSource } = this.props;
return (
<div className="lowcode-plugin-datasource-pane-datasource">
<SchemaForm
onSubmit={this.handleFormSubmit}
components={{
ArrayTable,
ParamValue,
Input,
NumberPicker,
Switch,
JSFunction,
}}
labelCol={3}
wrapperCol={21}
schema={this.deriveSchema()}
initialValues={this.deriveInitialData(dataSource)}
>
<FormButtonGroup offset={7}>
<Submit></Submit>
</FormButtonGroup>
</SchemaForm>
</div>
);
}
}

View File

@ -0,0 +1,122 @@
/**
*
*/
/* import React, { PureComponent, ReactElement, FC } from 'react';
import { Button, Input, Radio, NumberPicker, Switch, Icon } from '@alifd/next';
import { ArrayTable } from '@formily/next-components';
import { connect } from '@formily/react-schema-renderer';
import _isPlainObject from 'lodash/isPlainObject';
import _isArray from 'lodash/isArray';
import _isNumber from 'lodash/isNumber';
import _isString from 'lodash/isString';
import _isBoolean from 'lodash/isBoolean';
import _get from 'lodash/get';
import _tap from 'lodash/tap';
import { ExpressionSetter } from '@ali/lowcode-editor-setters';
const { Group: RadioGroup } = Radio;
export interface ExpressionProps {
className: string;
value: any;
onChange?: () => void;
type: 'string' | 'number' | 'boolan' | 'array';
}
export interface ExpressionState {
useExpression: false;
}
class ExpressionComp extends PureComponent<ExpressionProps, ExpressionState> {
static isFieldComponent = true;
state = {
useExpression: '',
};
constructor(props) {
super(props);
this.state.useExpression = this.isUseExpression(this.props.value);
this.handleChange = this.handleChange;
}
isUseExpression = (value: any) => {
if (_isPlainObject(value) && value.type === 'JSFunction') {
return true;
}
return false;
};
// @todo 需要再 bind 一次?
handleChange = (value) => {
this.props?.onChange(value);
}
componentDidUpdate(prevProps) {
if (this.props.value !== prevProps.value) {
this.setState({
value: this.props.value,
useExpression: this.isUseExpression(this.props.value),
});
}
}
handleUseExpressionChange = (useExpression) => {
this.setState(({ value }) => {
let nextValue = value || '';
if (useExpression) {
nextValue = {
type: 'JSFunction',
value: ''
};
} else {
nextVaule = null;
}
return {
value: nextValue,
useExpression,
};
});
};
renderOriginal = () => {
const { value, type } = this.props;
if (type === 'string') {
return <Input onChange={this.handleChange} value={value} />;
} else if (type === 'boolean') {
return <Switch onChange={this.handleChange} checked={value} />;
} else if (type === 'number') {
return <NumberPicker onChange={this.handleChange} value={value} />;
} else if (type === 'array') {
return <ArrayTable onChange={this.handleChange} value={value} />;
}
return null;
};
renderExpression = () => {
const { value, type } = this.props;
// @todo 传入上下文才有智能提示
return (
<ExpressionSetter value={value} onChange={this.handleChange} />
);
};
render() {
const { useExpression } = this.state;
return (
<div>
{!useExpression && this.renderOriginal()}
{useExpression && this.renderExpression()}
<Button onClick={this.handleUseExpressionChange}><Icon type="edit" /></Button>
</div>
);
}
}
export const Expression = connect({
getProps: (componentProps, fieldProps) => {
debugger;
}
})(ExpressionComp); */

View File

@ -0,0 +1,2 @@
export * from './param-value';
export * from './jsfunction';

View File

@ -0,0 +1,66 @@
import React, { PureComponent, createRef } from 'react';
import { Button, Input, Radio, NumberPicker, Switch } from '@alifd/next';
import { connect } from '@formily/react-schema-renderer';
import _isPlainObject from 'lodash/isPlainObject';
import _isArray from 'lodash/isArray';
import _isNumber from 'lodash/isNumber';
import _isString from 'lodash/isString';
import _isBoolean from 'lodash/isBoolean';
import _get from 'lodash/get';
import _tap from 'lodash/tap';
import MonacoEditor, { EditorWillMount } from 'react-monaco-editor';
const { Group: RadioGroup } = Radio;
export interface JSFunctionProps {
className: string;
value: any;
onChange?: () => void;
}
export interface JSFunctionState {
}
class JSFunctionComp extends PureComponent<JSFunctionProps, JSFunctionState> {
static isFieldComponent = true;
private monacoRef = createRef<editor.IStandaloneCodeEditor>();
constructor(props) {
super(props);
this.handleEditorChange = this.handleEditorChange.bind(this);
}
handleEditorChange = () => {
if (this.monacoRef.current) {
if (
!(this.monacoRef.current as editor.IStandaloneCodeEditor)
.getModelMarkers()
.find((marker: editor.IMarker) => marker.owner === 'json')
) {
this.props?.onChange(this.monacoRef.current?.getModels()?.[0]?.getValue());
}
}
};
handleEditorWillMount: EditorWillMount = (editor) => {
(this.monacoRef as MutableRefObject<editor.IStandaloneCodeEditor>).current = editor?.editor;
};
render() {
const { value, onChange } = this.props;
return (
<MonacoEditor
theme="vs-dark"
width={400}
height={150}
defaulvValue={value}
language="js"
onChange={this.handleEditorChange}
editorWillMount={this.handleEditorWillMount}
/>
);
}
}
export const JSFunction = connect()(JSFunctionComp);

View File

@ -0,0 +1,103 @@
import React, { PureComponent, ReactElement, FC } from 'react';
import { Button, Input, Radio, NumberPicker, Switch } from '@alifd/next';
import { connect } from '@formily/react-schema-renderer';
import _isPlainObject from 'lodash/isPlainObject';
import _isArray from 'lodash/isArray';
import _isNumber from 'lodash/isNumber';
import _isString from 'lodash/isString';
import _isBoolean from 'lodash/isBoolean';
import _get from 'lodash/get';
import _tap from 'lodash/tap';
import { ExpressionSetter } from '@ali/lowcode-editor-setters';
const { Group: RadioGroup } = Radio;
export interface ParamValueProps {
className: string;
value: any;
onChange?: () => void;
}
export interface ParamValueState {
type: 'string' | 'number' | 'boolean' | '';
}
class ParamValueComp extends PureComponent<ParamValueProps, ParamValueState> {
static isFieldComponent = true;
state = {
type: '',
};
constructor(props) {
super(props);
this.state.type = this.getTypeFromValue(this.props.value);
}
getTypeFromValue = (value) => {
if (_isBoolean(value)) {
return 'boolean';
} else if (_isNumber(value)) {
return 'number';
} else if (_isPlainObject(value) && value.type === 'JSFunction') {
return 'expression';
}
return 'string';
};
// @todo 需要再 bind 一次?
handleChange = (value) => {
this.props?.onChange(value);
}
componentDidUpdate(prevProps) {
if (this.props.value !== prevProps.value) {
this.setState({
value: this.props.value,
type: this.getTypeFromValue(this.props.value),
});
}
}
handleTypeChange = (type) => {
this.setState(({ value }) => {
let nextValue = value || '';
if (type === 'string') {
nextValue = nextValue.toString();
} else if (type === 'number') {
nextValue = nextValue * 1;
} else if (type === 'boolean') {
nextValue = nextValue === 'true' || nextValue;
} else if (type === 'expression') {
nextValue = '';
}
return {
value: nextValue,
type,
};
});
};
render() {
const { type } = this.state;
const { value } = this.props;
return (
<div>
{
<RadioGroup shape="button" size="small" onChange={this.handleTypeChange}>
<Radio value="string"></Radio>
<Radio value="boolean"></Radio>
<Radio value="number"></Radio>
<Radio value="expression"></Radio>
</RadioGroup>
}
{type === 'string' && <Input onChange={this.handleChange.bind(this)} value={value} />}
{type === 'boolean' && <Switch onChange={this.handleChange.bind(this)} checked={value} />}
{type === 'number' && <NumberPicker onChange={this.handleChange.bind(this)} value={value} />}
{type === 'expression' && <ExpressionSetter onChange={this.handleChange.bind(this)} value={value} />}
</div>
);
}
}
export const ParamValue = connect()(ParamValueComp);

View File

@ -0,0 +1,138 @@
/**
*
* @todo editor types
*/
import React, { PureComponent, createRef, MutableRefObject } from 'react';
import { Button } from '@alifd/next';
import _noop from 'lodash/noop';
import _isArray from 'lodash/isArray';
import _last from 'lodash/last';
import _isPlainObject from 'lodash/isPlainObject';
import MonacoEditor, { EditorWillMount } from 'react-monaco-editor';
import { editor } from 'monaco-editor';
import { DataSourceConfig } from '@ali/lowcode-types';
import Ajv from 'ajv';
import { DataSourcePaneImportPluginComponentProps } from '../types';
export interface DataSourceImportPluginCodeProps extends DataSourcePaneImportPluginComponentProps {
defaultValue?: DataSourceConfig[];
}
export interface DataSourceImportPluginCodeState {
code: string;
isCodeValid: boolean;
}
export class DataSourceImportPluginCode extends PureComponent<
DataSourceImportPluginCodeProps,
DataSourceImportPluginCodeState
> {
static defaultProps = {
defaultValue: [
{
type: 'http',
id: 'test',
},
]
};
state = {
code: '',
isCodeValid: true,
};
private monacoRef = createRef<editor.IStandaloneCodeEditor>();
constructor(props: DataSourceImportPluginCodeProps) {
super(props);
this.state.code = JSON.stringify(this.deriveValue(this.props.defaultValue));
this.handleEditorWillMount = this.handleEditorWillMount.bind(this);
this.handleEditorChange = this.handleEditorChange.bind(this);
this.handleComplete = this.handleComplete.bind(this);
}
deriveValue = (value: any) => {
const { dataSourceTypes } = this.props;
if (!_isArray(dataSourceTypes) || dataSourceTypes.length === 0) return [];
let result = value;
if (_isPlainObject(result)) {
// 如果是对象则转化成数组
result = [result];
} else if (!_isArray(result)) {
return [];
}
const ajv = new Ajv();
return (result as DataSourceConfig[]).filter((dataSource) => {
if (!dataSource.type) return false;
const dataSourceType = dataSourceTypes.find((type) => type.type === dataSource.type);
if (!dataSourceType) return false;
return ajv.validate(dataSourceType.schema, dataSource);
});
};
handleComplete = () => {
if (this.monacoRef.current) {
if (
!(this.monacoRef.current as editor.IStandaloneCodeEditor)
.getModelMarkers()
.find((marker: editor.IMarker) => marker.owner === 'json')
) {
this.setState({ isCodeValid: true });
this.props?.onImport(this.deriveValue(JSON.parse(_last(this.monacoRef.current.getModels()).getValue())));
return;
}
}
this.setState({ isCodeValid: false });
};
handleEditorChange = () => {
if (this.monacoRef.current) {
if (
!(this.monacoRef.current as editor.IStandaloneCodeEditor)
.getModelMarkers()
.find((marker: editor.IMarker) => marker.owner === 'json')
) {
this.setState({ isCodeValid: true });
}
}
};
handleEditorWillMount: EditorWillMount = (editor) => {
(this.monacoRef as MutableRefObject<editor.IStandaloneCodeEditor>).current = editor?.editor;
// @todo 格式化一次
};
handleCodeChagne = (code) => {
this.setState({ code });
}
render() {
const { onCancel = _noop } = this.props;
const { code, isCodeValid } = this.state;
return (
<div>
<MonacoEditor
theme="vs-dark"
width={800}
height={600}
defaultValue={code}
language="json"
onChange={this.handleEditorChange}
editorWillMount={this.handleEditorWillMount}
formatOnType
formatOnPaste
/>
{!isCodeValid && <p></p>}
<p>
<Button onClick={onCancel}></Button>
<Button onClick={this.handleComplete}></Button>
</p>
</div>
);
}
}

View File

@ -0,0 +1 @@
export * from './code';

View File

@ -0,0 +1,18 @@
.lowcode-plugin-datasource-pane {
margin: 0 8px;
>.next-tabs {
>.next-tabs-bar {
.next-tabs-nav-extra {
.next-btn {
margin-left: 4px;
}
}
}
}
}
.lowcode-plugin-datasource-pane-list {
.next-virtual-list-wrapper {
min-height: 400px;
}
}

View File

@ -0,0 +1,114 @@
import React, { PureComponent } from 'react';
import { PluginProps } from '@ali/lowcode-types';
import { DataSourcePane } from './pane';
import { DataSourcePaneImportPlugin, DataSourceType } from './types';
import { DataSourceImportPluginCode } from './import-plugins';
export { DataSourceImportPluginCode };
const PLUGIN_NAME = 'dataSourcePane';
export interface DataSourcePaneProps extends PluginProps {
importPlugins: DataSourcePaneImportPlugin[];
dataSourceTypes?: DataSourceType[];
}
export interface DataSourcePaneState {
active: boolean;
}
const BUILTIN_DATASOURCE_TYPES = [
{
type: 'http',
schema: {
type: 'object',
properties: {
options: {
type: 'object',
properties: {},
},
},
},
},
{
type: 'mtop',
schema: {
type: 'object',
properties: {
options: {
type: 'object',
properties: {
uri: {
title: 'api',
},
v: {
title: 'v',
type: 'string',
},
appKey: {
title: 'appKey',
type: 'string',
},
},
},
},
},
},
];
const BUILTIN_IMPORT_PLUGINS = [
{
name: 'default',
title: '源码',
component: DataSourceImportPluginCode,
},
];
export default class DataSourcePanePlugin extends PureComponent<DataSourcePaneProps, DataSourcePaneState> {
static displayName = 'DataSourcePanePlugin';
static defaultProps = {
dataSourceTypes: [],
importPlugins: [],
};
state = {
active: false,
};
constructor(props: DataSourcePaneProps) {
super(props);
this.state.active = true;
const { editor } = this.props;
// @todo pluginName, to unsubscribe
// 第一次 active 事件不会触发监听器
editor.on('skeleton.panel-dock.active', (pluginName, dock) => {
if (pluginName === PLUGIN_NAME) {
this.setState({ active: true });
}
});
editor.on('skeleton.panel-dock.unactive', (pluginName, dock) => {
if (pluginName === PLUGIN_NAME) {
this.setState({ active: false });
}
});
}
render() {
const { importPlugins, dataSourceTypes, editor } = this.props;
const { active } = this.state;
if (!active) return null;
const defaultSchema = editor.get('designer').project?.currentDocument?.schema?.dataSource ?? {};
return (
<DataSourcePane
importPlugins={BUILTIN_IMPORT_PLUGINS.concat(importPlugins)}
dataSourceTypes={BUILTIN_DATASOURCE_TYPES.concat(dataSourceTypes)}
defaultSchema={defaultSchema}
/>
);
}
}

View File

@ -0,0 +1,143 @@
import React, { PureComponent, ReactElement, FC } from 'react';
import { Button, Search, VirtualList, Tag, Balloon, Table } from '@alifd/next';
import { DataSourceConfig } from '@ali/lowcode-types';
import _isPlainObject from 'lodash/isPlainObject';
import _isNumber from 'lodash/isNumber';
import _isBoolean from 'lodash/isBoolean';
import _isNil from 'lodash/isNil';
import _tap from 'lodash/tap';
import { DataSourceType } from './types';
const { Column: TableCol } = Table;
export interface DataSourceListProps {
dataSourceTypes: DataSourceType[];
dataSource: DataSourceConfig[];
onEditDataSource?: (dataSourceId: string) => void;
onDuplicateDataSource?: (dataSourceId: string) => void;
onRemoveDataSource?: (dataSourceId: string) => void;
}
export interface DataSourceListState {
filteredType: string;
keyword: string;
}
type TableRow = {
label: string;
value: any;
};
export default class DataSourceList extends PureComponent<DataSourceListProps, DataSourceListState> {
state = {
filteredType: '',
keyword: '',
};
handleSearchFilterChange = (filteredType: any) => {
this.setState({ filteredType });
};
handleSearch = (keyword: any) => {
this.setState({ keyword });
};
handleEditDataSource = (id: any) => {
this.props.onEditDataSource?.(id);
};
handleDuplicateDataSource = (id: any) => {
this.props.onDuplicateDataSource?.(id);
};
handleRemoveDataSource = (id: any) => {
this.props.onRemoveDataSource?.(id);
};
deriveListDataSource = () => {
const { filteredType, keyword } = this.state;
const { dataSource } = this.props;
return (
dataSource
?.filter((item) => (filteredType ? item.type === filteredType : true))
?.filter((item) => (keyword ? item.id.indexOf(keyword) !== -1 : true))
?.map((item) => (
<li key={item.id}>
<Balloon
trigger={
<div>
<Tag size="small">{item.type}</Tag>
<Tag size="small">{item.isInit ? '自动' : '手动'}</Tag>
{item.id}
<Button onClick={this.handleEditDataSource.bind(this, item.id)}></Button>
<Button onClick={this.handleDuplicateDataSource.bind(this, item.id)}></Button>
<Button onClick={this.handleRemoveDataSource.bind(this, item.id)}></Button>
</div>
}
align="r"
alignEdge
triggerType="hover"
style={{ width: 300 }}
>
<Table
dataSource={_tap(Object.keys(item.options || {}).reduce<TableRow[]>((acc, cur) => {
// @todo 这里的 ts 处理得不好
if (_isPlainObject(item.options[cur])) {
Object.keys(item?.options?.[cur] || {}).forEach((curInOption) => {
acc.push({
label: `${cur}.${curInOption}`,
value: (item?.options?.[cur] as any)?.[curInOption],
});
});
} else if (!_isNil(item.options[cur])) {
// @todo 排除 null
acc.push({
label: cur,
value: item.options[cur],
});
}
return acc;
}, []), console.log)}
>
<TableCol title="" dataIndex="label" />
<TableCol
title=""
dataIndex="value"
cell={(val: any) => (
<div>
<Tag>
{_isBoolean(val) ? 'bool' : _isNumber(val) ? 'number' : _isPlainObject(val) ? 'obj' : 'string'}
</Tag>
{val.toString()}
</div>
)}
/>
</Table>
</Balloon>
</li>
)) || []
);
};
render() {
const { dataSourceTypes } = this.props;
return (
<div className="lowcode-plugin-datasource-pane-list">
<Search
hasClear
onSearch={this.handleSearch}
filterProps={{}}
defaultFilterValue={dataSourceTypes?.[0]?.type}
filter={dataSourceTypes.map((type) => ({
label: type?.type,
value: type?.type,
}))}
onFilterChange={this.handleSearchFilterChange}
/>
<VirtualList>{this.deriveListDataSource()}</VirtualList>
</div>
);
}
}

View File

@ -0,0 +1,3 @@
{
"DataSourcePane": "DataSource Pane"
}

View File

@ -0,0 +1,10 @@
import { createIntl } from '@ali/lowcode-editor-core';
import en_US from './en-US.json';
import zh_CN from './zh-CN.json';
const { intl, intlNode, getLocale, setLocale } = createIntl({
'en-US': en_US,
'zh-CN': zh_CN,
});
export { intl, intlNode, getLocale, setLocale };

View File

@ -0,0 +1,3 @@
{
"DataSourcePane": "数据源面板"
}

View File

@ -0,0 +1,344 @@
/**
* Dialog
*/
import React, { PureComponent } from 'react';
import { DataSource, DataSourceConfig } from '@ali/lowcode-types';
import { Tab, Button, MenuButton, Message } from '@alifd/next';
import _cloneDeep from 'lodash/cloneDeep';
import _uniqueId from 'lodash/uniqueId';
import _startsWith from 'lodash/startsWith';
import _isArray from 'lodash/isArray';
import _get from 'lodash/get';
import List from './list';
// import { DataSourceImportButton, DataSourceImportPluginCode } from './import';
import { DataSourceForm } from './datasource-form';
import { DataSourcePaneImportPlugin, DataSourceType } from './types';
const { Item: TabItem } = Tab;
const { Item: MenuButtonItem } = MenuButton;
const TAB_ITEM_LIST = 'list';
const TAB_ITEM_IMPORT = 'import';
const TAB_ITEM_CREATE = 'create';
const TAB_ITEM_EDIT = 'edit';
export interface DataSourcePaneProps {
dataSourceTypes?: DataSourceType[];
importPlugins?: DataSourcePaneImportPlugin[];
defaultSchema?: DataSource;
onSchemaChange?: (schema: DataSource) => void;
}
export interface TabItemProps {
key: string;
title: string;
closeable: boolean;
data: any;
}
export interface TabItem {
tabItemProps: TabItemProps;
}
export interface DataSourcePaneState {
dataSourceList: DataSourceConfig[];
tabItems: TabItem[];
activeTabKey: string;
}
export class DataSourcePane extends PureComponent<DataSourcePaneProps, DataSourcePaneState> {
state = {
dataSourceList: [...(this.props.defaultSchema?.list || [])],
tabItems: [
{
tabItemProps: {
key: TAB_ITEM_LIST,
title: '数据源列表',
closeable: false,
},
},
],
activeTabKey: TAB_ITEM_LIST,
};
constructor(props) {
super(props);
this.handleDataSourceListChange = this.handleDataSourceListChange.bind(this);
this.handleImportDataSourceList = this.handleImportDataSourceList.bind(this);
this.handleCreateDataSource = this.handleCreateDataSource.bind(this);
this.handleUpdateDataSource = this.handleUpdateDataSource.bind(this);
this.handleRemoveDataSource = this.handleRemoveDataSource.bind(this);
this.handleDuplicateDataSource = this.handleDuplicateDataSource.bind(this);
this.handleEditDataSource = this.handleEditDataSource.bind(this);
this.handleTabChange = this.handleTabChange.bind(this);
}
handleDataSourceListChange = (dataSourceList?: DataSourceConfig[]) => {
if (dataSourceList) {
this.setState({ dataSourceList });
}
this.props.onSchemaChange?.({
list: this.state.dataSourceList,
});
};
handleImportDataSourceList = (toImport: DataSourceConfig[]) => {
this.closeTab(TAB_ITEM_IMPORT);
this.setState(
({ dataSourceList }) => ({
dataSourceList: dataSourceList.concat(toImport),
}),
() => {
this.handleDataSourceListChange();
},
);
};
handleCreateDataSource = (toCreate: DataSourceConfig) => {
this.closeTab(TAB_ITEM_CREATE);
this.setState(
({ dataSourceList }) => ({
dataSourceList: dataSourceList.concat([
{
...toCreate,
},
]),
}),
() => {
this.handleDataSourceListChange();
},
);
};
handleUpdateDataSource = (toUpdate: DataSourceConfig) => {
this.closeTab(TAB_ITEM_EDIT);
this.setState(
({ dataSourceList }) => {
const nextDataSourceList = [...dataSourceList];
const dataSourceUpdate = nextDataSourceList.find((dataSource) => dataSource.id === toUpdate.id);
if (dataSourceUpdate) {
Object.assign(dataSourceUpdate, toUpdate);
}
return {
dataSourceList: nextDataSourceList,
};
},
() => {
this.handleDataSourceListChange();
},
);
};
handleRemoveDataSource = (dataSourceId: string) => {
this.setState(
({ dataSourceList }) => ({
dataSourceList: dataSourceList.filter((item) => item.id !== dataSourceId),
}),
() => {
this.handleDataSourceListChange();
},
);
};
handleDuplicateDataSource = (dataSourceId: string) => {
const target = this.state.dataSourceList.find((item) => item.id === dataSourceId);
const cloned = _cloneDeep(target);
this.openCreateDataSourceTab({
...cloned,
id: _uniqueId(`${cloned.id}_`),
});
};
handleEditDataSource = (dataSourceId: string) => {
const target = this.state.dataSourceList.find((item) => item.id === dataSourceId);
const cloned = _cloneDeep(target);
this.openEditDataSourceTab({
...cloned,
});
};
// @todo 没有识别出类型
handleTabChange = (activeTabKey: any) => {
this.setState({ activeTabKey });
};
openCreateDataSourceTab = (dataSourceTypeName: string) => {
const { tabItems } = this.state;
const { dataSourceTypes } = this.props;
if (!tabItems.find((item) => item.tabItemProps.key === TAB_ITEM_CREATE)) {
this.setState(({ tabItems }) => ({
tabItems: tabItems.concat({
tabItemProps: {
key: TAB_ITEM_CREATE,
title: `添加数据源`,
closeable: true,
data: {
dataSourceType: dataSourceTypes?.find(
type => type.type === dataSourceTypeName
),
},
},
}),
}));
this.setState({ activeTabKey: TAB_ITEM_CREATE });
} else {
Message.notice('当前已有一个新建数据源的 TAB 被大家');
}
};
openEditDataSourceTab = (dataSource: DataSourceConfig) => {
const { tabItems } = this.state;
const { dataSourceTypes } = this.props;
if (!tabItems.find((item) => item.tabItemProps.key === TAB_ITEM_EDIT)) {
this.setState(({ tabItems }) => ({
tabItems: tabItems.concat({
tabItemProps: {
key: TAB_ITEM_EDIT,
title: '修改数据源',
closeable: true,
data: {
dataSource,
dataSourceType: dataSourceTypes?.find(
type => type.type === dataSource.type
),
},
},
}),
}));
}
this.setState({ activeTabKey: TAB_ITEM_EDIT });
};
openImportDataSourceTab = (selectedImportPluginName) => {
const { tabItems } = this.state;
const { importPlugins } = this.props;
if (!tabItems.find((item) => item.tabItemProps.key === `${TAB_ITEM_IMPORT}_${selectedImportPluginName}`)) {
this.setState(({ tabItems }) => ({
tabItems: tabItems.concat({
tabItemProps: {
key: TAB_ITEM_IMPORT,
title: `导入数据源-${selectedImportPluginName}`,
closeable: true,
content: _get(
importPlugins?.find((plugin) => selectedImportPluginName === plugin.name),
'component',
),
},
}),
}));
this.setState({ activeTabKey: TAB_ITEM_IMPORT });
} else {
Message.notice('当前已有一个导入数据源的 TAB 被大家');
}
};
closeTab = (tabKey: any) => {
this.setState(
({ tabItems }) => ({
tabItems: tabItems.filter((item) => item.tabItemProps.key !== tabKey),
}),
() => {
this.setState(({ tabItems }) => ({
activeTabKey: _get(tabItems, '[0].tabItemProps.key')
}));
},
);
};
renderTabExtraContent = () => {
const { importPlugins, dataSourceTypes } = this.props;
// @todo onSelect 不行?
return [
_isArray(dataSourceTypes) && dataSourceTypes.length > 1 ? (
<MenuButton size="small" label="新建" onItemClick={this.openCreateDataSourceTab}>
{dataSourceTypes.map((type) => (
<MenuButtonItem key={type.type}>{type.type}</MenuButtonItem>
))}
</MenuButton>
) : _isArray(dataSourceTypes) && dataSourceTypes.length > 1 ? (
<Button onClick={this.openImportDataSourceTab.bind(this, dataSourceTypes[0])}></Button>
) : null,
_isArray(importPlugins) && importPlugins.length > 1 ? (
<MenuButton size="small" label="导入" onItemClick={this.openImportDataSourceTab}>
{importPlugins.map((plugin) => (
<MenuButtonItem key={plugin.name}>{plugin.name}</MenuButtonItem>
))}
</MenuButton>
) : _isArray(importPlugins) && importPlugins.length > 1 ? (
<Button onClick={this.openImportDataSourceTab.bind(this, importPlugins[0].name)}></Button>
) : null,
];
};
// 更通用的处理
renderTabItemContentByKey = (tabItemKey: any, data: any) => {
const { dataSourceList, tabItems } = this.state;
const { dataSourceTypes } = this.props;
if (tabItemKey === TAB_ITEM_LIST) {
return (
<List
dataSourceTypes={dataSourceTypes}
dataSource={dataSourceList}
onEditDataSource={this.handleEditDataSource}
onDuplicateDataSource={this.handleDuplicateDataSource}
onRemoveDataSource={this.handleRemoveDataSource}
/>
);
} else if (tabItemKey === TAB_ITEM_EDIT) {
const dataSourceType = dataSourceTypes.find((type) => type.type === data?.dataSource.type);
return (
<DataSourceForm
dataSourceType={dataSourceType}
dataSource={data?.dataSource}
onComplete={this.handleUpdateDataSource}
onCancel={this.closeTab.bind(this, TAB_ITEM_EDIT)}
/>
);
} else if (tabItemKey === TAB_ITEM_CREATE) {
const tabItemData = tabItems.find((tabItem) => tabItem.tabItemProps.key === tabItemKey);
return (
<DataSourceForm
dataSourceType={data?.dataSourceType}
onComplete={this.handleCreateDataSource}
onCancel={this.closeTab.bind(this, tabItemKey)}
/>
);
} else if (tabItemKey === TAB_ITEM_IMPORT) {
const tabItemData = tabItems.find((tabItem) => tabItem.tabItemProps.key === tabItemKey);
if (tabItemData) {
const Content = tabItemData.tabItemProps.content;
return <Content dataSourceTypes={dataSourceTypes} onCancel={this.closeTab.bind(this, tabItemKey)} onImport={this.handleImportDataSourceList} />;
}
}
return null;
};
render() {
const { dataSourceList, activeTabKey, tabItems } = this.state;
const { importPlugins, dataSourceTypes } = this.props;
return (
<div className="lowcode-plugin-datasource-pane">
<Tab
activeKey={activeTabKey}
extra={this.renderTabExtraContent()}
onChange={this.handleTabChange}
onClose={this.closeTab}
>
{tabItems.map((item) => (
<TabItem {...item.tabItemProps}>
{this.renderTabItemContentByKey(item.tabItemProps.key, item.tabItemProps.data)}
</TabItem>
))}
</Tab>
</div>
);
}
}

View File

@ -0,0 +1,6 @@
import { JSONSchema4 } from '@types/json-schema';
export type DataSourceType = {
type: string;
schema: JSONSchema4
};

View File

@ -0,0 +1,20 @@
import { DataSourceConfig } from '@ali/lowcode-types';
import { DataSourceType } from './datasource-type';
// 导入插件
export interface DataSourcePaneImportPlugin {
name: string;
title: string;
component: React.ReactNode;
componentProps?: DataSourcePaneImportPluginCustomProps;
}
export interface DataSourcePaneImportPluginCustomProps {
[customPropName: string]: any;
}
export interface DataSourcePaneImportPluginComponentProps extends DataSourcePaneImportPluginCustomProps {
dataSourceTypes: DataSourceType[];
onImport?: (dataSourceList: DataSourceConfig[]) => void;
onCancel?: () => void;
}

View File

@ -0,0 +1,2 @@
export * from './datasource-type';
export * from './import-plugin';

View File

@ -0,0 +1,5 @@
import test from 'ava';
test('foobar', t => {
t.pass();
});

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"lib": ["es6", "dom"],
"compilerOptions": {
"outDir": "lib"
},
"include": [
"./src/"
]
}