blob: 0826c642cba78bd560dfd4bfae4170799ff9122b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './shapes';
import type { Cell, Model } from '@antv/x6';
import { Addon, FunctionExt, Graph } from '@antv/x6';
import { notification } from 'antd';
import { formatMessage } from 'umi';
import {
DEFAULT_OPINIONS,
DEFAULT_PLUGIN_FLOW_DATA,
DEFAULT_STENCIL_OPINIONS,
FlowGraphEvent,
FlowGraphShape,
} from '../../constants';
class FlowGraph {
public static graph: Graph;
private static stencil: Addon.Stencil;
private static pluginTypeList: string[] = [];
private static plugins: PluginComponent.Meta[] = [];
public static init(
container: HTMLElement,
plugins: PluginComponent.Meta[] = [],
chart: Model.FromJSONData,
) {
this.graph = new Graph({
container,
...DEFAULT_OPINIONS,
});
this.plugins = plugins;
this.pluginTypeList = Array.from(new Set(plugins.map((item) => item.type)));
this.initStencil();
this.initShape();
this.initGraphShape(chart);
this.initEvent();
return this.graph;
}
// NOTE: set cell data according to Cell ID
public static setData(id: string, data: any): void {
const cell = this.graph.getCell(id);
if (cell) {
cell.setData(data, { overwrite: true });
}
}
// NOTE: Generate groups for stencil
private static generateGroups(): Addon.Stencil.Group[] {
const otherGroupList = [
{
name: 'basic',
title: formatMessage({ id: 'component.plugin-flow.text.general' }),
graphHeight: 104,
},
];
const pluginGroupList = this.pluginTypeList.map((item) => {
const count = this.plugins.filter((plugin) => plugin.type === item).length;
return {
name: item,
title: formatMessage({ id: `component.plugin.${item}` }),
layoutOptions: {
columns: 1,
marginX: 60,
},
graphHeight: count * 82,
};
});
return otherGroupList.concat(pluginGroupList);
}
private static initStencil() {
this.stencil = new Addon.Stencil({
target: this.graph,
...DEFAULT_STENCIL_OPINIONS,
groups: this.generateGroups(),
});
const stencilContainer = document.querySelector('#stencil');
stencilContainer?.appendChild(this.stencil.container);
}
private static initShape() {
const { graph } = this;
const r1 = graph.createNode({
shape: FlowGraphShape.start,
attrs: {
body: {
rx: 24,
ry: 24,
},
text: {
textWrap: {
text: formatMessage({ id: 'component.plugin-flow.text.start-node' }),
},
},
},
});
const r3 = graph.createNode({
shape: FlowGraphShape.condition,
width: 58,
height: 58,
angle: 45,
attrs: {
text: {
textWrap: {
text: formatMessage({ id: 'component.plugin-flow.text.condition2' }),
},
transform: 'rotate(-45deg)',
},
},
ports: {
groups: {
top: {
position: {
name: 'top',
args: {
dx: -26,
},
},
},
right: {
position: {
name: 'right',
args: {
dy: -26,
},
},
},
bottom: {
position: {
name: 'bottom',
args: {
dx: 26,
},
},
},
left: {
position: {
name: 'left',
args: {
dy: 26,
},
},
},
},
},
});
this.stencil.load([r1, r3], 'basic');
this.pluginTypeList.forEach((type) => {
const plugins = this.plugins
.filter((plugin) => plugin.type === type)
.map((plugin) => {
return graph.createNode({
shape: FlowGraphShape.plugin,
attrs: {
title: {
text: plugin.name,
},
text: {
text: plugin.name,
},
},
});
});
this.stencil.load(plugins, type);
});
}
private static initGraphShape(chart: Model.FromJSONData) {
if (!chart) {
return;
}
this.graph.fromJSON(chart);
}
private static showPorts(ports: NodeListOf<SVGAElement>, show: boolean) {
// eslint-disable-next-line
for (let i = 0, len = ports.length; i < len; i = i + 1) {
// eslint-disable-next-line
ports[i].style.visibility = show ? 'visible' : 'hidden';
}
}
private static initEvent() {
const { graph } = this;
const container = document.getElementById('container')!;
graph.on(
'node:mouseenter',
FunctionExt.debounce(() => {
const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGAElement>;
this.showPorts(ports, true);
}),
500,
);
graph.on('node:mouseleave', () => {
const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGAElement>;
this.showPorts(ports, false);
});
graph.on('node:dblclick', ({ node }) => {
if (node.shape === FlowGraphShape.plugin) {
const name = node.getAttrByPath('text/text') as string;
if (!name) {
return;
}
this.graph.trigger(FlowGraphEvent.PLUGIN_CHANGE, {
visible: true,
id: node.id,
name,
data: node.getData(),
});
}
if (node.shape === FlowGraphShape.condition) {
this.graph.trigger(FlowGraphEvent.CONDITION_CHANGE, {
id: node.id,
data: node.getData(),
visible: true,
});
}
});
graph.bindKey('backspace', () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.removeCells(cells);
}
});
}
private static getNextCell(id = '', position = ''): Cell.Properties | undefined {
const { cells = [] } = this.graph.toJSON();
const cell = cells.find((item) => item.id === id);
if (!cell) {
return undefined;
}
if (!cell.ports) {
return undefined;
}
const port = cell.ports.items.find((item: { group: string }) => item.group === position);
if (!port) {
return undefined;
}
const targetCellId = cells.find(
(item) => item.source?.port === port.id && item.source?.cell === id,
)?.target.cell;
const targetCell = cells.find((item) => item.id === targetCellId);
return targetCell;
}
private static getLeafList(currentId = '') {
let ids: string[] = [];
const fn = (id: string) => {
const cell = this.getNextCell(id, 'right');
if (!cell || !cell.id) {
return;
}
ids = ids.concat(cell.id);
fn(cell.id);
};
fn(currentId);
return [currentId].concat(ids);
}
/**
* Convert Graph JSON Data to API Request Body Data
*/
public static convertToData(
chart: typeof DEFAULT_PLUGIN_FLOW_DATA.chart | undefined = undefined,
):
| {
chart: {
cells: Cell.Properties[];
};
conf: Record<string, any>;
rule: Record<string, any>;
}
| undefined {
const data = {
...DEFAULT_PLUGIN_FLOW_DATA,
chart: chart || this.graph.toJSON(),
};
const { cells = [] } = data.chart;
const edgeCells = cells.filter((cell) => cell.shape === 'edge');
const startCell = cells.find((cell) => cell.shape === FlowGraphShape.start);
if (!startCell) {
notification.warn({
message: formatMessage({ id: 'component.plugin-flow.text.no-start-node' }),
});
return;
}
const rootCell = cells.find(
(cell) => cell.shape === 'edge' && cell.source.cell === startCell.id,
);
if (!rootCell) {
notification.warn({
message: formatMessage({ id: 'component.plugin-flow.text.no-root-node' }),
});
return;
}
data.rule.root = rootCell.target.cell;
// Get the ID associated with each node, the relationship between nodes is in edgeCells.
edgeCells.forEach((edge) => {
const sourceId = edge.source.cell;
const targetId = edge.target.cell;
data.rule[sourceId] = [];
this.getLeafList(targetId).forEach((id) => {
const cell = cells.find((item) => item.id === id);
if (!cell) {
return;
}
if (cell.shape === FlowGraphShape.condition) {
const nextCell = this.getNextCell(cell.id, 'bottom');
if (!nextCell) {
return;
}
data.rule[sourceId].push([cell.data, nextCell.id]);
}
if (cell.shape === FlowGraphShape.plugin) {
data.rule[sourceId].push(['', cell.id]);
}
});
});
// NOTE: Omit empty array, or API will throw error.
Object.entries(data.rule).forEach(([key, value]) => {
if (value.length === 0) {
delete data.rule[key];
}
if (key === 'root') {
return;
}
const cell = cells.find((item) => item.id === key);
if (cell?.shape !== FlowGraphShape.plugin) {
delete data.rule[key];
}
});
const invalidPluginCell = cells.find(
(item) => item.shape === FlowGraphShape.plugin && !item.data,
);
if (invalidPluginCell) {
notification.warn({
message: formatMessage({ id: 'component.plugin-flow.text.without-data' }),
description: `${formatMessage({
id: 'component.plugin-flow.text.plugin-without-data.description',
})}${invalidPluginCell.attrs?.text.text}`,
});
return;
}
const invalidConditionCell = cells.find(
(item) => item.shape === FlowGraphShape.condition && !item.data,
);
if (invalidConditionCell) {
notification.warn({
message: formatMessage({ id: 'component.plugin-flow.text.without-data' }),
description: `${formatMessage({
id: 'component.plugin-flow.text.condition-without-configuration',
})}`,
});
return;
}
data.conf = {};
cells
.filter((item) => item.shape === FlowGraphShape.plugin && item.id)
.forEach((item) => {
if (item.id) {
data.conf[item.id] = {
name: item.attrs?.text.text,
conf: item.data,
};
}
});
// eslint-disable-next-line
return data;
}
}
export default FlowGraph;