diff --git a/packages/gi-sdk/.umirc.ts b/packages/gi-sdk/.umirc.ts new file mode 100644 index 000000000..a87aae82d --- /dev/null +++ b/packages/gi-sdk/.umirc.ts @@ -0,0 +1,16 @@ +export default { + resolve: { includes: ['docs'] }, + externals: { + react: 'window.React', + 'react-dom': 'window.ReactDOM', + '@antv/g6': 'window.G6V5', + antd: 'window.antd', + }, + links: [{ href: 'https://gw.alipayobjects.com/os/lib/antd/4.24.14/dist/antd.css', rel: 'stylesheet' }], + scripts: [ + 'https://gw.alipayobjects.com/os/lib/react/17.0.2/umd/react.development.js', + 'https://gw.alipayobjects.com/os/lib/react-dom/17.0.2/umd/react-dom.development.js', + 'https://gw.alipayobjects.com/os/lib/antv/g6/5.0.0-beta.11/dist/g6.min.js', + 'https://gw.alipayobjects.com/os/lib/antd/4.24.14/dist/antd.min.js', + ], +}; diff --git a/packages/gi-sdk/README.md b/packages/gi-sdk/README.md index f388a8712..8c0516c9a 100644 --- a/packages/gi-sdk/README.md +++ b/packages/gi-sdk/README.md @@ -1,66 +1,7 @@ ## GISDK 组件 -> WIP - -## Studio 组件 - -通过接口,获得应用渲染的配置参数,从而让用户可以这么实现 - ```jsx -export default () => { - return ; -}; -``` - -组件属性说明 - -- id:studio 的 唯一 id,可以通过接口获得全部的配置参数 - -- service: 第三方托管平台提供的接口,类型:`POST` - -```js -// 入参 -const request = { - id: 'app-xxx', -}; -// 出参 -const response = { - workbook: { - id: 'd3a818ea-f833-4229-85ea-a6670dae4a18', - name: 'GI', - activeAssetsKeys: {}, - projectConfig: { - nodes: [], - edges: [], - components: [], - }, - themes: [], - }, - dataset: { - id: 'ds_f16fce61-aa3e-420d-a6cc-6b409f9aa37e', - engineContext: { - engineId: 'R+', - schemaData: {}, - }, - data: {}, - }, - deps: { - react: '17.x', - 'react-dom': '17.x', - localforage: '1.10.0', - antd: '4.24.8', - '@antv/gi-theme-antd': '0.4.2', - '@antv/g6': '4.8.14', - '@antv/graphin': '2.7.16', - '@antv/gi-sdk': '2.3.5', - }, - GI_ASSETS_PACKAGES: { - GI_ASSETS_BASIC: { - name: '@antv/gi-assets-basic', - version: '2.3.6', - url: 'https://gw.alipayobjects.com/os/lib/antv/gi-assets-basic/2.3.6/dist/index.min.js', - global: 'GI_ASSETS_BASIC', - }, - }, -}; +import React from 'react'; +import DEMO from './docs'; +export default DEMO; ``` diff --git a/packages/gi-sdk/docs/index.tsx b/packages/gi-sdk/docs/index.tsx new file mode 100644 index 000000000..06a294b44 --- /dev/null +++ b/packages/gi-sdk/docs/index.tsx @@ -0,0 +1,124 @@ +import GISDK, { Initializer, SimpleEdge, SimpleNode, useContext } from '@antv/gi-sdk'; +import { Utils } from '@antv/graphin'; +import React from 'react'; +interface DEMOProps {} + +const Counter = props => { + const { graph, updateContext } = useContext(); + + const { title } = props; + const nodes = graph.getAllNodesData().length; + console.log('Counter render....', nodes); + const handleClick = () => { + const newData = Utils.mock(Math.round(Math.random() * 300)) + .circle() + .graphin(); + + const preData = graph.getAllNodesData().length; + console.log('Pre Counts', preData); + + updateContext(draft => { + draft.data = newData; + }); + }; + return ( +
+ {title}: {nodes} + +
+ ); +}; + +const CounterAsset = { + info: { + id: 'Counter', + type: 'AUTO', + }, + registerMeta: () => { + return {}; + }, + component: Counter, +}; + +const assets = { + elements: { + SimpleEdge, + SimpleNode, + }, + layouts: {}, + components: { + Initializer, + Counter: CounterAsset, + }, +}; + +const config = { + nodes: [{ id: 'SimpleNode' }], + edges: [{ id: 'SimpleEdge' }], + components: [ + { + id: 'Initializer', + type: 'INITIALIZER', + name: '初始化器', + props: { + serviceId: 'GI/GI_SERVICE_INTIAL_GRAPH', + schemaServiceId: 'GI/GI_SERVICE_SCHEMA', + GI_INITIALIZER: true, + aggregate: false, + transByFieldMapping: false, + }, + }, + { + id: 'Counter', + type: 'AUTO', + props: { + title: '画布节点数量', + }, + }, + ], + layout: { + id: 'Concentric', + props: { + type: 'concentric', + }, + }, + pageLayout: {}, +}; + +const services = [ + { + name: '初始化查询', + method: 'GET', + id: 'GI/GI_SERVICE_INTIAL_GRAPH', + service: async () => { + return new Promise(resolve => { + resolve({ + nodes: [ + { id: 'node-1', data: {} }, + { id: 'node-2', data: {} }, + ], + edges: [], + }); + }); + }, + }, + { + name: '查询图模型', + method: 'GET', + id: 'GI/GI_SERVICE_SCHEMA', + service: async () => { + return new Promise(resolve => { + resolve({ + nodes: [], + edges: [], + }); + }); + }, + }, +]; + +const DEMO: React.FunctionComponent = props => { + return ; +}; + +export default DEMO; diff --git a/packages/gi-sdk/package.json b/packages/gi-sdk/package.json index 2142f9ff4..348525543 100644 --- a/packages/gi-sdk/package.json +++ b/packages/gi-sdk/package.json @@ -19,6 +19,7 @@ "*.less" ], "scripts": { + "docs": "dumi dev", "build": "npm run clean && npm run build:es & npm run build:umd", "build:es": "father build", "build:umd": "webpack --mode production -c ../../webpack.config.js --env path=/packages/gi-sdk", @@ -31,8 +32,9 @@ "postpublish": "tnpm sync @antv/gi-sdk" }, "dependencies": { + "dumi": "^1.1.50", "@antv/graphin": "workspace:*", - "@antv/g6": "5.0.0-beta.3", + "@antv/g6": "5.0.0-beta.5", "@aligov/global-locale": "^1.0.5", "@aligov/global-string-format": "^1.0.7", "@ant-design/icons": "^4.7.0", diff --git a/packages/gi-sdk/src/GISDK.tsx b/packages/gi-sdk/src/GISDK.tsx index 348847102..5ece0c2b6 100644 --- a/packages/gi-sdk/src/GISDK.tsx +++ b/packages/gi-sdk/src/GISDK.tsx @@ -1,6 +1,6 @@ import Graphin, { GraphinContext } from '@antv/graphin'; import { original } from 'immer'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { IntlProvider, useIntl } from 'react-intl'; import { useImmer } from 'use-immer'; import { defaultInitializerCfg } from './Initializer'; @@ -12,10 +12,55 @@ import type { GIComponentConfig, GIGraphData, Props, State } from './typing'; let updateHistoryTimer: number; +let HAS_INIT = false; + +const getComponentsCfg = (componentsCfg, pageLayout) => { + let GICC_LAYOUT = { id: 'EmptyLayout', props: {} }; + let INITIALIZER; + if (pageLayout) GICC_LAYOUT = pageLayout; + componentsCfg.forEach(item => { + if (pageLayout) { + if (item.id === pageLayout.id) { + GICC_LAYOUT = item; + return; + } + } else if (item.type === 'GICC_LAYOUT') { + GICC_LAYOUT = item; + return; + } + if (item.type === 'INITIALIZER' || item.props.GI_INITIALIZER) { + INITIALIZER = item; + return; + } + }); + return { componentsCfg, GICC_LAYOUT, INITIALIZER }; +}; + +const getTransformer = (nodesCfg, edgesCfg, ElementAssets) => { + /** + * + * @param data 源数据 + * @param reset 是否重置:按照 Node/Edge Schema来视觉映射 + * @returns + */ + const transform = (data, reset?: boolean) => { + const nodes = utils.transDataByConfig('nodes', data, { nodes: nodesCfg, edges: edgesCfg }, ElementAssets, reset); + const edges = utils.transDataByConfig('edges', data, { nodes: nodesCfg, edges: edgesCfg }, ElementAssets, reset); + + const { combos, tableResult } = data; + + return { + nodes, + edges, + combos, + tableResult, + }; + }; + return transform; +}; /** export */ const GISDK = (props: Props) => { - const graphinRef = React.useRef(null); - // @ts-ignore + const graphRef = useRef(null); const { children, assets, id, services, config, locales } = props; const { language = 'zh-CN', ...localeMessages } = locales || {}; @@ -41,6 +86,7 @@ const GISDK = (props: Props) => { nodes: [], edges: [], }, + HAS_GRAPH: false, source: { nodes: [], edges: [] } as GIGraphData, layout: {}, components: [] as GIComponentConfig[], @@ -61,11 +107,59 @@ const GISDK = (props: Props) => { GISDK_ID, }); - React.useEffect(() => { - updateState(draft => { - draft.config = props.config; - }); - }, [props.config]); + const { data, layout, components, initializer, theme, transform, GICC_LAYOUT, HAS_GRAPH, graph } = state; + + // const handleGraphInit = graph => { + // updateState(draft => { + // draft.graph = graph; + // draft.HAS_GRAPH = true; + // }); + // }; + + useEffect(() => { + // init... + if (!HAS_INIT) { + console.log('%c GISDK INTI ....', 'color:rgba(255,87,34,0.8)', graphRef); + HAS_INIT = true; + const { assets, config, services } = props; + + const { GICC_LAYOUT, INITIALIZER, componentsCfg } = getComponentsCfg(config.components, config.pageLayout); + const transform = getTransformer(config.nodes, config.edges, ElementAssets); + + updateState(draft => { + /** initializer */ + if (INITIALIZER.id !== draft.initializer?.id) { + draft.initializer = INITIALIZER; + } + /** components */ + draft.config.components = componentsCfg; + draft.components = componentsCfg; + /** layout */ + draft.config.layout = config.layout; + draft.layout = config.layout.props || {}; + draft.layoutCache = false; + /** styling */ + draft.transform = transform; + draft.config.nodes = config.nodes; + draft.config.edges = config.edges; + if (draft.data.nodes.length !== 0) { + const preData = original(draft.data); + // 当节点和边的Schema配置变化的时候,默认是重置视觉映射; + const newData = transform(preData, true); + //@ts-ignore + draft.data = newData; + } + + draft.GICC_LAYOUT = GICC_LAYOUT; + /** props */ + draft.config = config; + draft.servives = services; + /** flag */ + draft.graph = graphRef.current; + draft.HAS_GRAPH = true; + }); + } + }, []); const { layout: layoutCfg, @@ -77,28 +171,15 @@ const GISDK = (props: Props) => { /** 根据注册的图元素,生成Transform函数 */ React.useEffect(() => { - let GICC_LAYOUT = { id: 'EmptyLayout', props: {} }; - let INITIALIZER; - if (pageLayout) GICC_LAYOUT = pageLayout; - componentsCfg.forEach(item => { - if (pageLayout) { - if (item.id === pageLayout.id) { - GICC_LAYOUT = item; - return; - } - } else if (item.type === 'GICC_LAYOUT') { - GICC_LAYOUT = item; - return; - } - if (item.type === 'INITIALIZER' || item.props.GI_INITIALIZER) { - INITIALIZER = item; - return; - } - }); + if (!HAS_GRAPH) { + return; + } + console.log('%c GISDK COMPONENTS ....', 'color:rgba(255,87,34,0.8)'); + const { GICC_LAYOUT, INITIALIZER, componentsCfg: ComponentCfg } = getComponentsCfg(componentsCfg, pageLayout); updateState(draft => { - draft.config.components = componentsCfg; - draft.components = componentsCfg; + draft.config.components = ComponentCfg; + draft.components = ComponentCfg; if (INITIALIZER.id !== draft.initializer?.id) { //@ts-ignore draft.initializer = INITIALIZER; @@ -107,74 +188,16 @@ const GISDK = (props: Props) => { //@ts-ignore draft.GICC_LAYOUT = GICC_LAYOUT; }); - }, [componentsCfg, pageLayout]); + }, [componentsCfg, pageLayout, HAS_GRAPH]); React.useEffect(() => { - if (!layoutCfg) { + if (!layoutCfg || !HAS_GRAPH) { return; } + console.log('%c GISDK LAYOUT ....', 'color:rgba(255,87,34,0.8)'); stopForceSimulation(); - // @ts-ignore const { type, ...options } = layoutCfg.props || {}; - //@ts-ignore let otherOptions = {}; - if (options && options.defSpringLenCfg) { - //@ts-ignore - otherOptions = { - defSpringLen: utils.getDefSpringLenFunction(options.defSpringLenCfg), - }; - } - // 资金力导布局定制 - if (layoutCfg.id === 'FundForce') { - otherOptions = { - defSideCoe: utils.getDefSideCoeFunction(options.income, options.outcome, options.isLog, options.multiple), - }; - } - - if (layoutCfg.id === 'Force2') { - const { - advanceWeight, - edgeWeightField, - nodeWeightField, - nodeWeightFieldFromEdge, - nodeWeightFromType, - directed, - directedFromType, - directedInWeightField, - directedOutWeightField, - directedIsLog, - directedMultiple, - directedAmountFromEdge, - } = options; - (otherOptions as any).defSideCoe = 'unset'; - if (advanceWeight) { - if (edgeWeightField) { - (otherOptions as any).edgeStrength = utils.getEdgeWeightedStrength(options); - } - if ( - (nodeWeightFromType === 'node' && nodeWeightField) || - (nodeWeightFromType === 'edge' && nodeWeightFieldFromEdge) - ) { - (otherOptions as any).nodeStrength = utils.getNodeWeightedStrength(options); - } - if (directed) { - if (directedFromType === 'node') { - (otherOptions as any).defSideCoe = utils.getDefSideCoeFunction( - directedInWeightField, - directedOutWeightField, - directedIsLog, - directedMultiple, - ); - } else if (directedAmountFromEdge) { - (otherOptions as any).defSideCoe = utils.getDefSideCoeFromEdgeFunction( - directedAmountFromEdge, - directedIsLog, - directedMultiple, - ); - } - } - } - } updateState(draft => { draft.layout = { @@ -187,31 +210,15 @@ const GISDK = (props: Props) => { draft.config.layout = layoutCfg; draft.layoutCache = false; }); - }, [layoutCfg]); + }, [layoutCfg, HAS_GRAPH]); /** 增加多元素 */ React.useEffect(() => { - if (!nodesCfg || !edgesCfg || nodesCfg.length === 0 || edgesCfg.length === 0) { + if (!nodesCfg || !edgesCfg || nodesCfg.length === 0 || edgesCfg.length === 0 || !HAS_GRAPH) { return; } - - /** - * - * @param data 源数据 - * @param reset 是否重置:按照 Node/Edge Schema来视觉映射 - * @returns - */ - const transform = (data, reset?: boolean) => { - const nodes = utils.transDataByConfig('nodes', data, { nodes: nodesCfg, edges: edgesCfg }, ElementAssets, reset); - const edges = utils.transDataByConfig('edges', data, { nodes: nodesCfg, edges: edgesCfg }, ElementAssets, reset); - const { combos, tableResult } = data; - return { - nodes, - edges, - combos, - tableResult, - }; - }; + console.log('%c GISDK STYLE ....', 'color:rgba(255,87,34,0.8)'); + const transform = getTransformer(nodesCfg, edgesCfg, ElementAssets); updateState(draft => { if (draft.data.nodes.length !== 0) { @@ -221,15 +228,11 @@ const GISDK = (props: Props) => { //@ts-ignore draft.data = newData; } - draft.transform = transform; draft.config.nodes = nodesCfg; draft.config.edges = edgesCfg; }); - }, [nodesCfg, edgesCfg]); - - // @ts-ignore - const { data, layout, components, initializer, theme, transform, GICC_LAYOUT } = state; + }, [nodesCfg, edgesCfg, HAS_GRAPH]); // console.log('%c G6VP Render...', 'color:red', state.layout); const sourceDataMap = useMemo(() => { @@ -250,47 +253,46 @@ const GISDK = (props: Props) => { }, [state.source]); const stopForceSimulation = () => { - if (graphinRef.current) { - const { layout, graph } = graphinRef.current; - const { instance } = layout; - if (instance) { - const { type, simulation } = instance; - if (type === 'graphin-force') { - simulation.stop(); - return; - } - } - const layoutController = graph.get('layoutController'); - const layoutMethod = layoutController.layoutMethods?.[0]; - if (layoutMethod?.type === 'force2') { - layoutMethod.stop(); - } - } + // if (graphinRef.current) { + // const { layout, graph } = graphinRef.current; + // const { instance } = layout; + // if (instance) { + // const { type, simulation } = instance; + // if (type === 'graphin-force') { + // simulation.stop(); + // return; + // } + // } + // const layoutController = graph.get('layoutController'); + // const layoutMethod = layoutController.layoutMethods?.[0]; + // if (layoutMethod?.type === 'force2') { + // layoutMethod.stop(); + // } + // } }; const restartForceSimulation = (nodes = []) => { - if (graphinRef.current) { - const { layout: graphLayout, graph } = graphinRef.current; - const { instance } = graphLayout; - if (instance) { - const { type, simulation } = instance; - if (type === 'graphin-force') { - simulation.restart(nodes, graph); - return; - } - } - const layoutController = graph.get('layoutController'); - const layoutMethod = layoutController.layoutMethods?.[0]; - if (layoutMethod?.type === 'force2') { - graph.updateLayout({ animate: true, disableTriggerLayout: false }); - updateState(draft => { - draft.layout.animate = true; - }); - } - } + // if (graphinRef.current) { + // const { layout: graphLayout, graph } = graphinRef.current; + // const { instance } = graphLayout; + // if (instance) { + // const { type, simulation } = instance; + // if (type === 'graphin-force') { + // simulation.restart(nodes, graph); + // return; + // } + // } + // const layoutController = graph.get('layoutController'); + // const layoutMethod = layoutController.layoutMethods?.[0]; + // if (layoutMethod?.type === 'force2') { + // graph.updateLayout({ animate: true, disableTriggerLayout: false }); + // updateState(draft => { + // draft.layout.animate = true; + // }); + // } + // } }; - console.log('graphinRef.current', graphinRef.current); - const HAS_GRAPH = graphinRef.current?.graph && !graphinRef.current.graph.destroyed; + console.log('%c GISDK RENDER....', 'color:rgba(255,87,34,1)', HAS_GRAPH, state.initialized); const ContextValue = { ...state, GISDK_ID, @@ -298,10 +300,8 @@ const GISDK = (props: Props) => { assets, sourceDataMap, HAS_GRAPH, - graph: graphinRef.current?.graph, - theme: graphinRef.current?.theme, - apis: graphinRef.current?.apis, - layoutInstance: graphinRef.current?.layout, + graph: graph, + updateContext: updateState, updateData: res => { updateState(draft => { @@ -360,52 +360,26 @@ const GISDK = (props: Props) => { const { renderComponents, InitializerComponent, InitializerProps, GICC_LAYOUT_COMPONENT, GICC_LAYOUT_PROPS } = getComponents({ ...state, HAS_GRAPH }, config.components, ComponentAssets); - const graphData = useMemo(() => { - const nodeMap = {}; - const edgeMap = {}; - const edges: any[] = []; - const nodes: any[] = []; - data.nodes?.forEach(node => { - if (!nodeMap[node.id]) { - nodes.push(node); - nodeMap[node.id] = node; - } - }); - - data.edges.forEach(edge => { - if (nodeMap[edge.source] && nodeMap[edge.target] && !edgeMap[edge.id]) { - edges?.push(edge); - edgeMap[edge.id] = edge; - } - }); - const { combos } = data; - return { - nodes, - edges, - combos, - }; - }, [data]); - return (
+ {/* @ts-ignore */} {/* @ts-ignore */} + - <> - - {HAS_GRAPH && } - {HAS_GRAPH && state.initialized && renderComponents()} - {HAS_GRAPH && state.initialized && children} - + + {HAS_GRAPH && } + {HAS_GRAPH && state.initialized && renderComponents()} + {HAS_GRAPH && state.initialized && children} diff --git a/packages/gi-sdk/src/SetupUseGraphinHook.tsx b/packages/gi-sdk/src/SetupUseGraphinHook.tsx deleted file mode 100644 index 435ef0f2b..000000000 --- a/packages/gi-sdk/src/SetupUseGraphinHook.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { GraphinContext } from '@antv/graphin'; - -import React from 'react'; - -const SetupUseGraphinHook = props => { - const { updateContext } = props; - const { graph, apis, layout, theme } = React.useContext(GraphinContext); - - React.useEffect(() => { - const stopForceSimulation = () => { - const { instance } = layout; - if (instance) { - const { type, simulation } = instance; - if (type === 'graphin-force') { - simulation.stop(); - } - } - }; - const restartForceSimulation = (nodes = []) => { - const { instance } = layout; - if (instance) { - const { type, simulation } = instance; - if (type === 'graphin-force') { - simulation.restart(nodes, graph); - } - } - }; - - updateContext(draft => { - draft.graph = graph; - draft.theme = theme; - draft.apis = apis; - //@ts-ignore - draft.layoutInstance = layout; - draft.isContextReady = true; - draft.stopForceSimulation = stopForceSimulation; - draft.restartForceSimulation = restartForceSimulation; - }); - return () => { - // console.warn('卸载组件....', graph && graph.destroyed); - }; - }, [layout, graph]); - - return null; -}; - -export default SetupUseGraphinHook; diff --git a/packages/gi-sdk/src/components/EngineServer/LoadGraph.tsx b/packages/gi-sdk/src/components/EngineServer/LoadGraph.tsx index 57616e5dc..68b060f49 100644 --- a/packages/gi-sdk/src/components/EngineServer/LoadGraph.tsx +++ b/packages/gi-sdk/src/components/EngineServer/LoadGraph.tsx @@ -2,11 +2,12 @@ import Graphin from '@antv/graphin'; import { Button, Col, Form, Input, Row, Select, Statistic } from 'antd'; import * as React from 'react'; import { useImmer } from 'use-immer'; -import { GraphSchemaData, utils } from '../../index'; +import $i18n from '../../i18n'; +import { GraphSchemaData } from '../../index'; +import * as utils from '../../process'; import CollapseCard from '../CollapseCard'; import type { GraphDBConfig } from './index'; import { getEngineForm, setEngineForm } from './utils'; -import $i18n from '../../i18n'; const { getSchemaGraph } = utils; diff --git a/packages/gi-sdk/src/components/Initializer/Component.tsx b/packages/gi-sdk/src/components/Initializer/Component.tsx new file mode 100644 index 000000000..40079b8fc --- /dev/null +++ b/packages/gi-sdk/src/components/Initializer/Component.tsx @@ -0,0 +1,176 @@ +import { notification } from 'antd'; +import React, { memo } from 'react'; +import $i18n from '../../i18n'; +import { GIConfig, useContext } from '../../index'; +import * as utils from '../../process'; + +const { isPosition, isStyles } = utils; + +export type GIService = any; +export interface IProps { + serviceId: string; + schemaServiceId: string; + aggregate: boolean; + transByFieldMapping: boolean; +} + +const Initializer: React.FunctionComponent = props => { + const context = useContext(); + const { serviceId, schemaServiceId, aggregate, transByFieldMapping } = props; + const { services, updateContext, transform, largeGraphLimit } = context; + + React.useEffect(() => { + // const { service: initialService } = services.find(s => s.id === serviceId) as GIService; + // const { service: schemaService } = (services.find(s => s.id === schemaServiceId) as GIService) || { + // service: () => Promise.resolve(null), + // }; + + let initialService = services.find(s => s.id === serviceId) as GIService; + let schemaService = services.find(s => s.id === schemaServiceId) as GIService; + + if (!initialService) { + notification.error({ + message: $i18n.get({ id: 'basic.components.Initializer.Component.CanvasRenderingFailed', dm: '画布渲染失败' }), + description: $i18n.get( + { + id: 'basic.components.Initializer.Component.TheServiceidServiceIsMissing', + dm: '缺少 {serviceId} 服务,请检查相关资产是否加载成功', + }, + { serviceId: serviceId }, + ), + }); + initialService = { + service: () => { + return new Promise(resolve => { + resolve({ + nodes: [], + edges: [], + }); + }); + }, + }; + } + if (!schemaService) { + notification.error({ + message: $i18n.get({ + id: 'basic.components.Initializer.Component.FailedToObtainGraphModel', + dm: '图模型获取失败', + }), + description: $i18n.get( + { + id: 'basic.components.Initializer.Component.TheServiceidServiceIsMissing', + dm: '缺少 {serviceId} 服务,请检查相关资产是否加载成功', + }, + { serviceId: serviceId }, + ), + }); + schemaService = { + service: () => { + return new Promise(resolve => { + resolve({ + nodes: [], + edges: [], + }); + }); + }, + }; + } + updateContext(draft => { + draft.isLoading = true; + }); + + Promise.all([schemaService.service(), initialService.service()]).then( + ([schemaData, graphData = { nodes: [], edges: [] }]) => { + let schema = schemaData; + let data = graphData; + + if (transByFieldMapping) { + const { schemaData: _schemaData, data: _data } = utils.transDataBySchemaMeta(graphData, schemaData); + schema = _schemaData; + data = _data; + } + const { nodes } = data; + + if (nodes.length > largeGraphLimit) { + notification.warn({ + message: $i18n.get({ + id: 'basic.components.Initializer.Component.TheAmountOfDataLoaded', + dm: '加载的数据量过大', + }), + description: $i18n.get({ + id: 'basic.components.Initializer.Component.WeRecommendThatYouAggregate', + dm: '建议聚合数据,默认切换到网格布局。您也可以在「资产中心」中加载「大图组件」启用 3D 渲染', + }), + }); + } + updateContext(draft => { + /** 判断是否保存样式和位置 */ + const position = isPosition(nodes); + const style = isStyles(nodes); + /** 取消布局缓存 */ + draft.initialized = true; + draft.layoutCache = false; + + /** 如果接口有 schema,就更新 schemaData */ + if (schema) { + draft.schemaData = schema as any; + } + /** 只有当 config 中没有 nodes 和 edges 的时候,才会用 schema 生成一个默认样式 */ + + if (schema && (draft.config.nodes?.length === 0 || draft.config.edges?.length === 0)) { + const schemaStyle = utils.generatorStyleConfigBySchema(schema) as GIConfig; + draft.config.nodes = schemaStyle.nodes; + draft.config.edges = schemaStyle.edges; + } + /** 如果有布局信息 */ + if (position) { + draft.layout.type = 'preset'; + } + /** 如果有样式数据 */ + if (style) { + draft.data = data; + draft.source = data; + draft.isLoading = false; + return; + } + /** 如果是大图模式 */ + if (nodes.length > largeGraphLimit) { + const newData = transform(data, true); + draft.largeGraphMode = true; + draft.largeGraphData = newData; + draft.source = newData; + draft.data = { + nodes: [], + edges: [], + }; + draft.isLoading = false; + return; + } + /** 如果是聚合模式 */ + if (aggregate) { + const newData = transform(data, true); + draft.rawData = { ...data }; + draft.source = newData; + draft.largeGraphMode = false; + draft.largeGraphData = undefined; + draft.data = transform(utils.aggregateEdges(data), true); + draft.isLoading = false; + return; + } + /** 默认是普通模式 */ + const newData = transform(data, true); + draft.rawData = { ...data }; + draft.data = newData; + draft.source = newData; + draft.largeGraphMode = false; + draft.largeGraphData = undefined; + draft.isLoading = false; + }); + }, + ); + }, [largeGraphLimit, aggregate, transByFieldMapping]); + + return null; +}; + +export default memo(Initializer); diff --git a/packages/gi-sdk/src/components/Initializer/index.tsx b/packages/gi-sdk/src/components/Initializer/index.tsx new file mode 100644 index 000000000..390cc7dca --- /dev/null +++ b/packages/gi-sdk/src/components/Initializer/index.tsx @@ -0,0 +1,9 @@ +import Component from './Component'; +import info from './info'; +import registerMeta from './registerMeta'; + +export default { + info, + component: Component, + registerMeta, +}; diff --git a/packages/gi-sdk/src/components/Initializer/info.ts b/packages/gi-sdk/src/components/Initializer/info.ts new file mode 100644 index 000000000..fb3d565b9 --- /dev/null +++ b/packages/gi-sdk/src/components/Initializer/info.ts @@ -0,0 +1,17 @@ +import $i18n from '../../i18n'; +const info = { + id: 'Initializer', + name: $i18n.get({ id: 'basic.components.Initializer.info.Initializer', dm: '初始化器' }), + desc: $i18n.get({ + id: 'basic.components.Initializer.info.RequiredInitializeQueryGraphData', + dm: '必选!初始化查询图数据与图模型', + }), + // icon: 'icon-export', + cover: 'http://xxxx.jpg', + category: 'system-interaction', + type: 'INITIALIZER', + // 申明需要实现的服务名 + services: ['GI_SERVICE_INTIAL_GRAPH', 'GI_SERVICE_SCHEMA'], + docs: 'https://www.yuque.com/antv/gi/eedyuy', +}; +export default info; diff --git a/packages/gi-sdk/src/components/Initializer/registerMeta.ts b/packages/gi-sdk/src/components/Initializer/registerMeta.ts new file mode 100644 index 000000000..fce82b19b --- /dev/null +++ b/packages/gi-sdk/src/components/Initializer/registerMeta.ts @@ -0,0 +1,62 @@ +import $i18n from '../../i18n'; +import { utils } from '../../index'; +import info from './info'; + +export default context => { + const { services, engineId } = context; + const { options: initializerServiceOptions, defaultValue: defaultInitializerService } = + utils.getServiceOptionsByEngineId(services, info.services[0], engineId); + const { options: schemaServiceOptions, defaultValue: defaultschemaService } = utils.getServiceOptionsByEngineId( + services, + info.services[1], + engineId, + ); + return { + serviceId: { + title: $i18n.get({ id: 'basic.components.Initializer.registerMeta.InitializeAQuery', dm: '初始化查询' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + options: initializerServiceOptions, + }, + default: defaultInitializerService, + }, + schemaServiceId: { + title: $i18n.get({ id: 'basic.components.Initializer.registerMeta.QueryGraphModel', dm: '查询图模型' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + options: schemaServiceOptions, + }, + default: defaultschemaService, + }, + + // 注意⚠️:GI_INITIALIZER 是必须的属性字段,千万不要漏掉 + GI_INITIALIZER: { + title: $i18n.get({ id: 'basic.components.Initializer.registerMeta.DefaultStartup', dm: '默认启动' }), + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + 'x-component-props': { + disabled: true, + }, + default: true, + }, + aggregate: { + title: $i18n.get({ id: 'basic.components.Initializer.registerMeta.SummaryEdge', dm: '汇总边' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + default: false, + }, + transByFieldMapping: { + title: $i18n.get({ id: 'basic.components.Initializer.registerMeta.EnableFieldMapping', dm: '开启字段映射' }), + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + default: false, + }, + }; +}; diff --git a/packages/gi-sdk/src/components/SimpleEdge/index.tsx b/packages/gi-sdk/src/components/SimpleEdge/index.tsx new file mode 100644 index 000000000..d409ccd47 --- /dev/null +++ b/packages/gi-sdk/src/components/SimpleEdge/index.tsx @@ -0,0 +1,23 @@ +import registerMeta from './registerMeta'; +import registerTransform from './registerTransform'; +import $i18n from '../../i18n'; +const registerShape = Graphin => { + // graphinEdge 已经在内部注册成功 +}; +/** index.md 中解析得到默认值,也可用户手动修改 */ +const info = { + id: 'SimpleEdge', + category: 'edge', + name: $i18n.get({ id: 'basic.elements.SimpleEdge.OfficialSide', dm: '官方边' }), + desc: 'SimpleEdge', + cover: 'http://xxxx.jpg', + type: 'EDGE', + docs: 'https://www.yuque.com/antv/gi/ce260nnvfvagqszi', +}; + +export default { + info, + registerShape, + registerMeta, + registerTransform, +}; diff --git a/packages/gi-sdk/src/components/SimpleEdge/registerMeta.tsx b/packages/gi-sdk/src/components/SimpleEdge/registerMeta.tsx new file mode 100644 index 000000000..370c17dd7 --- /dev/null +++ b/packages/gi-sdk/src/components/SimpleEdge/registerMeta.tsx @@ -0,0 +1,327 @@ +import $i18n from '../../i18n'; +import { defaultConfig } from './registerTransform'; +const { advanced, color, size } = defaultConfig; +const { keyshape, label, animate } = advanced; + +const registerMeta = context => { + const { keys, schemaData } = context; + + const schema = { + type: 'object', + properties: { + color: { + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Color', dm: '颜色' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'ColorInput', + default: color, + }, + size: { + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Size', dm: '大小' }), + type: 'number', + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + default: size, + }, + label: { + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Text', dm: '文本' }), + type: 'string', + // enum: keys.map(c => { + // return { + // label: `${c.id} (${c.type})`, + // value: c.id, + // }; + // }), + 'x-decorator': 'FormItem', + 'x-component': 'GroupSelect', + 'x-component-props': { + mode: 'multiple', + schemaData: schemaData.edges, + }, + }, + advancedPanel: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'FormCollapse', + 'x-component-props': { + className: 'gi-assets-elements-advance-panel', + ghost: true, + }, + properties: { + advanced: { + type: 'object', + 'x-component': 'FormCollapse.CollapsePanel', + 'x-component-props': { + header: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.AdvancedConfiguration', dm: '高级配置' }), + key: 'advanced-panel', + }, + properties: { + panel: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'FormCollapse', + 'x-component-props': { + className: 'gi-assets-elements-panel', + style: {}, + ghost: true, + }, + properties: { + keyshape: { + type: 'object', + 'x-decorator': 'FormItem', + 'x-component': 'FormCollapse.CollapsePanel', + 'x-component-props': { + header: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Shape', dm: '形状' }), + key: 'icon-panel', + }, + properties: { + hasArrow: { + type: 'boolean', + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Arrow', dm: '箭头' }), + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + default: true, + }, + customPoly: { + type: 'boolean', + title: $i18n.get({ + id: 'basic.elements.SimpleEdge.registerMeta.DefineRadians', + dm: '定义弧度', + }), + default: keyshape.customPoly, + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + 'x-reactions': [ + { + target: 'advanced.keyshape.poly', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.icon.fill', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.icon.size', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + ], + }, + poly: { + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Radian', dm: '弧度' }), + type: 'number', + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + default: keyshape.poly, + }, + + lineDash: { + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.DottedLine', dm: '虚线' }), + type: 'array', + 'x-decorator': 'FormItem', + 'x-component': 'Offset', + 'x-component-props': { + min: -100, + max: 100, + }, + default: keyshape.lineDash, + }, + opacity: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Transparency', dm: '透明度' }), + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + default: keyshape.opacity, + }, + }, + }, + label: { + type: 'object', + 'x-decorator': 'FormItem', + 'x-component': 'FormCollapse.CollapsePanel', + 'x-component-props': { + header: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Label', dm: '标签' }), + key: 'keyshape-panel', + }, + properties: { + visible: { + type: 'boolean', + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Visible', dm: '显隐' }), + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + default: label.visible, + }, + fontSize: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Size', dm: '大小' }), + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + default: label.fontSize, + }, + offset: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Offset', dm: '偏移' }), + 'x-decorator': 'FormItem', + 'x-component': 'Offset', + 'x-component-props': { + min: -100, + max: 100, + }, + default: label.offset, + }, + fill: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Color', dm: '颜色' }), + 'x-decorator': 'FormItem', + 'x-component': 'ColorInput', + default: label.fill, + }, + backgroundEnable: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Background', dm: '背景' }), + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + default: label.backgroundEnable, + }, + backgroundFill: { + type: 'string', + title: $i18n.get({ + id: 'basic.elements.SimpleEdge.registerMeta.BackgroundColor', + dm: '背景色', + }), + 'x-decorator': 'FormItem', + 'x-component': 'ColorInput', + default: label.backgroundFill, + }, + backgroundStroke: { + type: 'string', + title: $i18n.get({ + id: 'basic.elements.SimpleEdge.registerMeta.BackgroundStroke', + dm: '背景描边', + }), + 'x-decorator': 'FormItem', + 'x-component': 'ColorInput', + default: label.backgroundStroke, + }, + }, + }, + animate: { + type: 'object', + 'x-component': 'FormCollapse.CollapsePanel', + 'x-component-props': { + header: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Animation', dm: '动画' }), + key: 'aniamte-panel', + }, + properties: { + visible: { + type: 'boolean', + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Switch', dm: '开关' }), + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + default: animate.visible, + 'x-reactions': [ + { + target: 'advanced.animate.type', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.animate.dotColor', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.animate.repeat', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.animate.duration', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + ], + }, + type: { + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Type', dm: '类型' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + enum: [ + { + label: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Ball', dm: '圆球' }), + value: 'circle-running', + }, + { + label: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.DottedLine', dm: '虚线' }), + value: 'line-dash', + }, + { + label: $i18n.get({ + id: 'basic.elements.SimpleEdge.registerMeta.GradualLength', + dm: '渐长', + }), + value: 'line-growth', + }, + ], + + default: animate.type, + }, + dotColor: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.BallColor', dm: '圆球颜色' }), + 'x-decorator': 'FormItem', + 'x-component': 'ColorInput', + default: animate.dotColor, + }, + repeat: { + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Repeat', dm: '重复' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + default: animate.repeat, + }, + duration: { + title: $i18n.get({ id: 'basic.elements.SimpleEdge.registerMeta.Duration', dm: '时长' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + default: animate.duration, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + return schema; +}; +export default registerMeta; diff --git a/packages/gi-sdk/src/components/SimpleEdge/registerShape.ts b/packages/gi-sdk/src/components/SimpleEdge/registerShape.ts new file mode 100644 index 000000000..0ffc499c1 --- /dev/null +++ b/packages/gi-sdk/src/components/SimpleEdge/registerShape.ts @@ -0,0 +1,4 @@ +const registerShape = Graphin => { + // graphinEdge 已经注册成功 +}; +export default registerShape; diff --git a/packages/gi-sdk/src/components/SimpleEdge/registerTransform.ts b/packages/gi-sdk/src/components/SimpleEdge/registerTransform.ts new file mode 100644 index 000000000..90ebdb62a --- /dev/null +++ b/packages/gi-sdk/src/components/SimpleEdge/registerTransform.ts @@ -0,0 +1,199 @@ +import { Utils } from '@antv/graphin'; +import type { GIEdgeConfig } from '../../index'; + +const defaultEdgeTheme = { + primaryEdgeColor: '#ddd', + edgeSize: 1, + mode: 'light' as 'light' | 'dark', +}; + +const defaultEdgeStyles = Utils.getEdgeStyleByTheme(defaultEdgeTheme); + +const { style } = defaultEdgeStyles; +const { keyshape, label } = style; + +export const defaultConfig = { + size: defaultEdgeTheme.edgeSize, + color: defaultEdgeTheme.primaryEdgeColor, + label: [], + advanced: { + keyshape: { + customPoly: false, + poly: 0, + lineDash: [0, 0], + // lineAppendWidth: keyshape.lineWidth, + opacity: keyshape.strokeOpacity, + hasArrow: true, + }, + label: { + visible: true, + fontSize: label.fontSize, + offset: [0, 0], + fill: label.fill, + backgroundEnable: true, + backgroundFill: '#fff', + backgroundStroke: '#fff', + backgroundOpaciy: 1, + opacity: 1, + }, + animate: { + visible: false, + type: 'circle-running', + dotColor: 'red', + repeat: true, + duration: 3000, + }, + }, + status: { + minZoom: { + label: { + opacity: 0, + }, + 'label-background': { + opacity: 0, + }, + }, + }, +}; + +export type EdgeConfig = typeof defaultConfig; + +/** 数据映射函数 需要根据配置自动生成*/ +const transform = (edges, config: GIEdgeConfig, reset?: boolean) => { + try { + const { color: color_CFG, size: size_CFG, label: LABEL_KEYS, advanced, status: defaultStatus } = defaultConfig; + + const { keyshape: keyshape_CFG } = advanced; + + const transEdge = (edge, index) => { + // properties + const { source, target } = edge; + const id = edge.id || `${source}-${target}-${index}`; + const data = edge.data || edge.properties || edge; + const isLoop = edge.source === edge.target; //edge.style && edge.style.keyshape && edge.style.keyshape.type === 'loop'; + const isPoly = edge.isMultiple; + let endArrow = {}; + const { customPoly, hasArrow } = keyshape_CFG; + if (!hasArrow) { + //@ts-ignore + endArrow = { + endArrow: { + path: '', + }, + }; + } + const shape: any = {}; + if (isLoop) { + shape.type = 'loop'; + shape.loop = { ...edge.style?.keyshape.loop }; + } + if (isPoly) { + shape.type = 'poly'; + shape.poly = { ...edge.style?.keyshape.poly }; + } + if (!isPoly && !isLoop) { + //只有直线的时候才支持设置弧度,多边的默认是系统分配的弧度 + shape.type = 'poly'; + shape.poly = { + distance: advanced.keyshape.poly, + }; + } + if (customPoly) { + //如果用户要强行自定义弧度,那就随他去吧 + shape.poly = { + distance: advanced.keyshape.poly, + }; + } + + /** LABEL */ + // const LABEL_VALUE = LABEL_KEYS.map(l => data[l]).join('_'); + + const LABEL_VALUE = LABEL_KEYS.map((d: string) => { + /** + * 兼容性处理:原先的label 逻辑是 ${type}.${properpertiesKey} + * 现在改为 ${type}^^${properpertiesKey} + */ + const newLabelArray = d.split('^^'); + const oldLabelArray = d.split('.'); + let [edgeType, propObjKey, propName] = newLabelArray; + const isOld = newLabelArray.length === 1 && newLabelArray[0].split('.').length > 1; + if (isOld) { + edgeType = oldLabelArray[0]; + propObjKey = oldLabelArray[1]; + propName = oldLabelArray[2]; + } + + // const [edgeType, propObjKey, propName] = d.split('^^'); + + // 只有当 nodeType 匹配时才取对应的属性值 + if (edge.edgeType || 'UNKNOW' === edgeType) { + // propName 存在,则 propObjKey 值一定为 properties + if (propName) { + return data[propObjKey][propName]; + } + /** 如果有汇总边,则强制使用汇总边的文本展示 */ + const { aggregate } = data; + if (aggregate) { + const sum = aggregate.reduce((acc, curr) => { + const val = curr.data[propObjKey]; + if (typeof val === 'number') { + acc = acc + val; + return acc; + } else { + return ''; + } + }, 0); + if (sum === '') { + return data['aggregateCount']; + } + return `(${aggregate.length} 条):${sum.toFixed(2)}`; + } + return data[propObjKey]; + } + + return data[edgeType]; + }) + .filter(d => d) + .join('\n'); + + const label: any = { + value: LABEL_VALUE, + offset: advanced.label.offset, + fontSize: advanced.label.fontSize, + fill: advanced.label.fill, + opacity: advanced.label.opacity, + }; + if (!advanced.label.visible) { + label.value = ''; + } + if (advanced.label.backgroundEnable) { + label.background = { + fill: advanced.label.backgroundFill, + stroke: advanced.label.backgroundStroke, + opacity: advanced.label.backgroundOpaciy, + }; + } + + let preStyle = (edge && edge.style) || {}; + + if (reset) { + preStyle = {}; + } + + return { + ...edge, + source, + target, + id, + data, + type: 'graphin-line', + edgeType: edge.edgeType || 'UNKOWN', + }; + }; + return transEdge; + } catch (error) { + console.error('parse transform error:', error); + return edge => edge; + } +}; +export default transform; diff --git a/packages/gi-sdk/src/components/SimpleNode/index.tsx b/packages/gi-sdk/src/components/SimpleNode/index.tsx new file mode 100644 index 000000000..d9ed4561c --- /dev/null +++ b/packages/gi-sdk/src/components/SimpleNode/index.tsx @@ -0,0 +1,23 @@ +import registerMeta from './registerMeta'; +import registerShape from './registerShape'; +import registerTransform, { defaultConfig } from './registerTransform'; + +/** index.md 中解析得到默认值,也可用户手动修改 */ import $i18n from '../../i18n'; +const info = { + id: 'SimpleNode', + category: 'node', + type: 'NODE', + name: $i18n.get({ id: 'basic.elements.SimpleNode.OfficialNode', dm: '官方节点' }), + icon: 'icon-smile', + desc: $i18n.get({ id: 'basic.elements.SimpleNode.OfficialNode', dm: '官方节点' }), + cover: 'https://gw.alipayobjects.com/mdn/rms_0d75e8/afts/img/A*myb8SrnSy0cAAAAAAAAAAAAAARQnAQ', + docs: 'https://www.yuque.com/antv/gi/mkrt58kk7m8qi0cu', +}; + +export default { + info, + defaultProps: defaultConfig, + registerShape, + registerMeta, + registerTransform, +}; diff --git a/packages/gi-sdk/src/components/SimpleNode/registerMeta.tsx b/packages/gi-sdk/src/components/SimpleNode/registerMeta.tsx new file mode 100644 index 000000000..eacb88064 --- /dev/null +++ b/packages/gi-sdk/src/components/SimpleNode/registerMeta.tsx @@ -0,0 +1,342 @@ +import $i18n from '../../i18n'; +import { defaultConfig } from './registerTransform'; + +const { icon, keyshape, label, badge } = defaultConfig.advanced; +const registerMeta = context => { + const { schemaData } = context; + const schema = { + type: 'object', + properties: { + size: { + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Size', dm: '大小' }), + type: 'number', + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + default: defaultConfig.size, + }, + color: { + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Color', dm: '颜色' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'ColorInput', + default: defaultConfig.color, + }, + label: { + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Text', dm: '文本' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'GroupSelect', + 'x-component-props': { + mode: 'multiple', + schemaData: schemaData.nodes, + }, + }, + advancedPanel: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'FormCollapse', + 'x-component-props': { + className: 'gi-assets-elements-advance-panel', + // style: { background: 'blue' }, + ghost: true, + }, + properties: { + advanced: { + type: 'object', + 'x-component': 'FormCollapse.CollapsePanel', + 'x-component-props': { + header: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.AdvancedConfiguration', dm: '高级配置' }), + // 暂时不设置高级配置默认收起,否则下面的 visible 控制就失效了 + key: 'advanced-panel', + }, + properties: { + panel: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'FormCollapse', + 'x-component-props': { + className: 'gi-assets-elements-panel', + style: { + // background: 'red', + // margin: '-16px', + }, + ghost: true, + }, + properties: { + icon: { + type: 'object', + 'x-decorator': 'FormItem', + 'x-component': 'FormCollapse.CollapsePanel', + 'x-component-props': { + header: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Icon', dm: '图标' }), + key: 'icon-panel', + }, + properties: { + visible: { + type: 'boolean', + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Visible', dm: '显隐' }), + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + 'x-reactions': [ + { + target: 'advanced.icon.type', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.icon.value', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.icon.fill', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.icon.size', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + ], + }, + type: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Type', dm: '类型' }), + 'x-decorator': 'FormItem', + 'x-component': 'Select', + enum: [ + { + label: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Text', dm: '文本' }), + value: 'text', + }, + { + label: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.FontIcon', dm: '字体图标' }), + value: 'font', + }, + ], + default: icon.type, + }, + value: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Icon', dm: '图标' }), + 'x-decorator': 'FormItem', + 'x-component': 'IconPicker', + default: icon.value, + }, + fill: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Color', dm: '颜色' }), + 'x-decorator': 'FormItem', + 'x-component': 'ColorInput', + default: icon.fill, + }, + // size: { + // type: 'string', + // title: '大小', + // 'x-decorator': 'FormItem', + // 'x-component': 'NumberPicker', + // default: icon.size, + // }, + }, + }, + keyshape: { + type: 'object', + 'x-decorator': 'FormItem', + 'x-component': 'FormCollapse.CollapsePanel', + 'x-component-props': { + header: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Node', dm: '节点' }), + key: 'keyshape-panel', + }, + properties: { + fillOpacity: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Transparency', dm: '透明度' }), + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + max: 1, + min: 0, + default: keyshape.fillOpacity, + }, + }, + }, + label: { + type: 'object', + 'x-component': 'FormCollapse.CollapsePanel', + 'x-component-props': { + header: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Text', dm: '文本' }), + key: 'label-panel', + }, + properties: { + visible: { + type: 'boolean', + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Switch', dm: '开关' }), + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + default: label.visible, + 'x-reactions': [ + { + target: 'advanced.label.fill', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.label.fontSize', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.label.position', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + ], + }, + fill: { + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Color', dm: '颜色' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'ColorInput', + default: label.fill, + }, + fontSize: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Size', dm: '大小' }), + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + max: 100, + min: 12, + default: label.fontSize, + }, + position: { + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Location', dm: '位置' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + enum: [ + { + label: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Top', dm: '顶部' }), + value: 'top', + }, + { + label: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Bottom', dm: '底部' }), + value: 'bottom', + }, + { + label: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.LeftSide', dm: '左侧' }), + value: 'left', + }, + { + label: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.RightSide', dm: '右侧' }), + value: 'right', + }, + { + label: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Middle', dm: '中间' }), + value: 'center', + }, + ], + + default: label.position, + }, + }, + }, + badge: { + type: 'object', + 'x-component': 'FormCollapse.CollapsePanel', + 'x-component-props': { + header: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Logo', dm: '徽标' }), + key: 'badge-panel', + }, + properties: { + visible: { + type: 'boolean', + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Visible', dm: '显隐' }), + 'x-decorator': 'FormItem', + 'x-component': 'Switch', + default: badge.visible, + 'x-reactions': [ + { + target: 'advanced.badge.type', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + { + target: 'advanced.badge.value', + fulfill: { + state: { + visible: '{{$self.value}}', + }, + }, + }, + ], + }, + type: { + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Type', dm: '类型' }), + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + enum: [ + { + label: $i18n.get({ + id: 'basic.elements.SimpleNode.registerMeta.FieldMapping', + dm: '字段映射', + }), + value: 'mapping', + }, + { + label: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Text', dm: '文本' }), + value: 'text', + }, + { + label: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.FontIcon', dm: '字体图标' }), + value: 'font', + }, + ], + + default: badge.type, + }, + value: { + type: 'string', + title: $i18n.get({ id: 'basic.elements.SimpleNode.registerMeta.Text', dm: '文本' }), + 'x-decorator': 'FormItem', + 'x-component': 'Input', + default: badge.value, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + return schema; +}; +export default registerMeta; diff --git a/packages/gi-sdk/src/components/SimpleNode/registerShape.ts b/packages/gi-sdk/src/components/SimpleNode/registerShape.ts new file mode 100644 index 000000000..ea8a3d952 --- /dev/null +++ b/packages/gi-sdk/src/components/SimpleNode/registerShape.ts @@ -0,0 +1,4 @@ +const registerShape = Graphin => { + // graphinNode 已经注册成功 +}; +export default registerShape; diff --git a/packages/gi-sdk/src/components/SimpleNode/registerTransform.ts b/packages/gi-sdk/src/components/SimpleNode/registerTransform.ts new file mode 100644 index 000000000..026f44c0f --- /dev/null +++ b/packages/gi-sdk/src/components/SimpleNode/registerTransform.ts @@ -0,0 +1,273 @@ +import { Utils } from '@antv/graphin'; +import type { GINodeConfig } from '../../index'; +import { icons } from '../../index'; + +const defaultNodeTheme = { + primaryColor: '#FF6A00', + nodeSize: 26, + mode: 'light' as 'light' | 'dark', +}; + +const getLabel = (data, LABEL_KEYS) => { + return LABEL_KEYS.map((d: string) => { + /** + * 兼容性处理:原先的label 逻辑是 ${type}.${properpertiesKey} + * 现在改为 ${type}^^${properpertiesKey} + */ + const [newNodeType, newLabelKey] = d.split('^^'); + const [oldNodeType, oldLabelKey] = d.split('.'); + const key = newLabelKey || oldLabelKey || 'id'; + return data[key]; + }) + .filter(d => d) + .join('\n'); +}; + +const getIconStyleByConfig = (style, data) => { + const { keyshape } = style; + if (!style.icon || !keyshape) { + return {}; + } + const icon = { ...style.icon }; + const { value } = icon; + + if (icon.visible) { + if (icon.type === 'image') { + return { + fill: 'transparent', + size: [keyshape.size, keyshape.size], + type: 'image', + clip: { r: keyshape.size / 2 }, + value: value, + }; + } + + if (icon.type === 'font') { + return { + ...icon, + size: keyshape.size / 2, + type: 'font', + fontFamily: 'iconfont', + value: icons[value] || '', + fill: icon.fill || keyshape.fill, + }; + } + if (icon.type === 'text') { + return { + ...icon, + fontSize: keyshape.size / 4, + fill: '#fff', + value: value, + }; + } + return { + ...icon, + }; + } + return { + ...icon, + visible: false, + value: '', + }; +}; + +const getBadgesStyleByConfig = (style, data) => { + const { badge, keyshape } = style; + if (!badge || !keyshape) { + return []; + } + + const { visible, value, color } = badge; + + if (visible) { + const size = Math.round(keyshape.size / 3); + const fontSize = size / 2; + badge.size = size; + badge.stroke = color || keyshape.stroke; + + if (badge.type === 'mapping') { + const b = { + type: 'text', + size, + stroke: keyshape.stroke, + fill: '#fff', + color: keyshape.fill, + visible: Boolean(data[value]), + value: data[value], + fontSize, + }; + return [b]; + } + if (badge.type === 'font') { + badge.type = 'font'; + badge.fontFamily = 'graphin'; + badge.value = icons[value] || ''; + } + if (badge.type === 'text') { + badge.fill = '#fff'; + badge.color = color || keyshape.fill; + badge.value = value; + badge.fontSize = fontSize; + } + return [badge]; + } + return []; +}; + +const defaultNodeStyles = Utils.getNodeStyleByTheme(defaultNodeTheme); + +const { style, status } = defaultNodeStyles; +const { keyshape, halo, label, icon } = style; + +export const defaultConfig = { + size: defaultNodeTheme.nodeSize, + color: defaultNodeTheme.primaryColor, + label: [], + advanced: { + keyshape: { + ...keyshape, + fillOpacity: 0.8, + }, + label: { + ...label, + opacity: 1, + visible: true, + }, + icon: { + ...icon, + fill: '#fff', + type: 'font', + value: '', + opacity: 1, + visible: false, + }, + badge: { + visible: false, + position: 'RT', + type: 'text', + value: '', + size: Math.round(keyshape.size / 3), // 徽标占据九宫格的最右上角,所以/3 + fill: '#fff', + color: '#fff', + stroke: keyshape.stroke, + isMapping: false, + }, + halo: { + ...halo, + visible: false, + lineWidth: 0, + }, + }, + status: { + minZoom: { + label: { + opacity: 0, + }, + icon: { + opacity: 0, + }, + badges: { + opacity: 0, + }, + }, + }, +}; +export type NodeConfig = typeof defaultConfig; + +/** 数据映射函数 需要根据配置自动生成*/ +const transform = (_nodes, nodeConfig: GINodeConfig, reset?: boolean) => { + try { + /** 解构配置项 */ + + const { color, size, label: LABEL_KEYS, advanced, status: userStatus } = defaultConfig; + + let isBug = false; + //@ts-ignore + if (!Object.is(advanced)) { + isBug = true; + } + const { halo } = isBug ? defaultConfig.advanced : advanced; + const transNode = node => { + // properties + const data = node.data || node.properties || node; + + const keyshape = { + ...advanced.keyshape, + fill: color, + stroke: color, + size: size, + }; + advanced.keyshape = keyshape; + const LABEL_VALUE = getLabel(data, LABEL_KEYS); + const icon = getIconStyleByConfig(advanced, data); + const badges = getBadgesStyleByConfig(advanced, data); + + const label = { + ...advanced.label, + value: advanced.label.visible ? LABEL_VALUE : '', + }; + + let preStyle = (node && node.style) || {}; + if (reset) { + preStyle = {}; + } + + const styleByConfig = { + keyshape, + label, + icon, + halo, + badges, + status: { + ...status, + ...userStatus, + highlight: { + keyshape: { + lineWidth: 4, + fillOpacity: 0.6, + }, + }, + active: { + halo: { + visible: true, + }, + keyshape: { + lineWidth: 5, + }, + }, + /** 扩散的状态 */ + query_start: { + halo: { + visible: true, + stroke: color, + lineWidth: 4, + lineDash: [8, 8], + }, + }, + query_normal: { + halo: { + visible: true, + stroke: color, + lineWidth: 1, + lineDash: [8, 8], + }, + }, + }, + }; + + return { + ...node, + id: node.id, + data, + nodeType: node.nodeType || 'UNKNOW', + type: 'circle-node', + // 数据中的style还是优先级最高的 + }; + }; + return transNode; + } catch (error) { + console.error('parse transform error:', error); + return node => node; + } +}; +export default transform; diff --git a/packages/gi-sdk/src/hooks/useComponents.tsx b/packages/gi-sdk/src/hooks/useComponents.tsx index 3c7d4c29a..105fd44c4 100644 --- a/packages/gi-sdk/src/hooks/useComponents.tsx +++ b/packages/gi-sdk/src/hooks/useComponents.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import DefaultInitializer from '../components/Initializer'; const DEFAULT_GICC_LAYOUT = { id: 'EmptyLayout', @@ -20,7 +21,7 @@ const useComponents = (state, propsComponentsCfg, ComponentAssets) => { }; }, {}); - const { component: InitializerComponent } = ComponentAssets[initializer.id]; + const { component: InitializerComponent } = ComponentAssets[initializer.id] || DefaultInitializer; const { props: InitializerProps } = ComponentCfgMap[initializer.id]; diff --git a/packages/gi-sdk/src/index.tsx b/packages/gi-sdk/src/index.tsx index 46bec5b0e..d461d442d 100644 --- a/packages/gi-sdk/src/index.tsx +++ b/packages/gi-sdk/src/index.tsx @@ -1,4 +1,3 @@ -/** export */ import pkg from '../package.json'; import { deepClone, @@ -9,6 +8,23 @@ import { GIAC_PROPS, } from './components/const'; import GIAComponent from './components/GIAC'; +export { Icon, icons } from '@antv/gi-common-components'; +export { Compatible } from '@antv/graphin'; +export { default as CollapseCard } from './components/CollapseCard'; +export type { IGIAC } from './components/const'; +export { default as EngineBanner } from './components/EngineBanner'; +export { default as EngineServer } from './components/EngineServer'; +/** default assets */ +export { default as Initializer } from './components/Initializer'; +export { default as SimpleEdge } from './components/SimpleEdge'; +export { default as SimpleNode } from './components/SimpleNode'; +/** default assets */ +export { default as Studio } from './components/Studio'; +export { Info } from './constants/info'; +export { default as useContainer } from './hooks/useContainer'; +export { Shortcuts, useShortcuts } from './utils'; +export { common }; + import template from './constants/template'; import { useContext } from './context'; import GISDK from './GISDK'; @@ -27,26 +43,16 @@ const extra = { deepClone, GIAComponent, }; -export { Icon, icons } from '@antv/gi-common-components'; -export { default as CollapseCard } from './components/CollapseCard'; -export type { IGIAC } from './components/const'; -export { default as EngineBanner } from './components/EngineBanner'; -export { default as EngineServer } from './components/EngineServer'; -export { default as Studio } from './components/Studio'; -export { Info } from './constants/info'; -export { default as useContainer } from './hooks/useContainer'; -export { Shortcuts, useShortcuts } from './utils'; -export { common }; const common = { createDownload, }; + // export { default as Icon } from './components/Icon'; /** export typing */ -export { COLORS, IEdgeSchema, INodeSchema } from './process/schema'; - export { changeLanguage, formatMessage, getCurrentLanguage, LANGUAGE_KEY_NAME } from './process/locale'; -export type { IGraphData } from './process/schema'; +export { COLORS } from './process/schema'; +export type { IEdgeSchema, IGraphData, INodeSchema } from './process/schema'; export type { AssetCategory, AssetInfo, diff --git a/packages/gi-sdk/src/process/loaderAssets.tsx b/packages/gi-sdk/src/process/loaderAssets.tsx index 5a3b005de..185b15994 100644 --- a/packages/gi-sdk/src/process/loaderAssets.tsx +++ b/packages/gi-sdk/src/process/loaderAssets.tsx @@ -42,6 +42,7 @@ export const loadJS = memoize(async (options: AssetPackage) => { }; }); }, JSON.stringify); + export const loader = async (options: AssetPackage[]) => { return Promise.all([ ...options.map(opt => { diff --git a/packages/gi-sdk/src/process/transDataByConfig.ts b/packages/gi-sdk/src/process/transDataByConfig.ts index 24ed00fa2..30a930e34 100644 --- a/packages/gi-sdk/src/process/transDataByConfig.ts +++ b/packages/gi-sdk/src/process/transDataByConfig.ts @@ -1,4 +1,6 @@ import { GraphinData, IUserEdge } from '@antv/graphin'; +import SimpleEdge from '../components/SimpleEdge'; +import SimpleNode from '../components/SimpleNode'; import { GIAssets, GIConfig } from '../typing'; import { uniqueElementsBy } from './common'; import { filterByRules } from './filterByRules'; @@ -49,7 +51,8 @@ export const transDataByConfig = ( } const Element = ElementAssets[id]; const filterData = filterByRules(elementData, { logic, expressions }); - return Element.registerTransform(filterData, item, reset); + const elementMapper = Element.registerTransform(filterData, item, reset); + return filterData.map(elementMapper); }) .reduce((acc, curr) => { return [...curr, ...acc]; @@ -63,9 +66,21 @@ export const transDataByConfig = ( const restElements = elementData.filter(n => { return uniqueIds.indexOf(n.id) === -1; }); + //@ts-ignore + let elementAsset = ElementAssets[basicConfig.id]; + if (!elementAsset) { + if (elementType === 'edges') { + //@ts-ignore + elementAsset = SimpleEdge; + } else { + //@ts-ignore + elementAsset = SimpleNode; + } + } //@ts-ignore - const restData = ElementAssets[basicConfig.id].registerTransform(restElements, basicConfig, reset); + const restMapper = elementAsset.registerTransform(restElements, basicConfig, reset); + const restData = restElements.map(restMapper); const nodes = [...uniqueElements, ...restData]; console.timeEnd(`${elementType.toUpperCase()}_TRANS_COST`); diff --git a/packages/gi-sdk/src/typing.ts b/packages/gi-sdk/src/typing.ts index bc7cd271f..665c61f33 100644 --- a/packages/gi-sdk/src/typing.ts +++ b/packages/gi-sdk/src/typing.ts @@ -101,7 +101,7 @@ export interface Props { schemaData?: GraphSchemaData; style?: React.CSSProperties; className?: string; - children?: React.ReactChildren | JSX.Element | JSX.Element[]; + children?: React.ReactNode[]; } export type AssetType =