feat: autolayout

This commit is contained in:
linonetwo 2023-07-16 12:41:44 +08:00 committed by lin onetwo
parent 18fcf66bc6
commit 0fe256ef59
10 changed files with 223 additions and 16 deletions

View file

@ -475,7 +475,8 @@
"ChangeMetadata": "Change Metadata", "ChangeMetadata": "Change Metadata",
"ChangeWorkflowMetadataDescription": "Create this automated workflow and save it to the selected workspace wiki to backup.", "ChangeWorkflowMetadataDescription": "Create this automated workflow and save it to the selected workspace wiki to backup.",
"ChangeWorkflowMetadataDoneMessage": "Change Done", "ChangeWorkflowMetadataDoneMessage": "Change Done",
"ToggleOnReadonly": "Toggle On Readonly" "ToggleOnReadonly": "Toggle On Readonly",
"AutoLayout": "Auto Layout"
}, },
"Description": "Description", "Description": "Description",
"Tags": "Tags", "Tags": "Tags",

View file

@ -479,6 +479,7 @@
"ChangeWorkflowMetadata": "修改本流程图的元信息", "ChangeWorkflowMetadata": "修改本流程图的元信息",
"ChangeMetadata": "修改元信息", "ChangeMetadata": "修改元信息",
"ChangeWorkflowMetadataDescription": "修改所选的自动化工作流并保存到所选的工作区Wiki里备份。", "ChangeWorkflowMetadataDescription": "修改所选的自动化工作流并保存到所选的工作区Wiki里备份。",
"ChangeSelectedItemInfo": "修改选中的物体的信息" "ChangeSelectedItemInfo": "修改选中的物体的信息",
"AutoLayout": "自动调整布局"
} }
} }

View file

@ -186,6 +186,8 @@
"glob": "^10.3.3", "glob": "^10.3.3",
"graphql-hooks": "6.2.3", "graphql-hooks": "6.2.3",
"json5": "^2.2.3", "json5": "^2.2.3",
"klayjs": "0.2.1",
"klayjs-noflo": "^0.3.1",
"node-loader": "2.0.0", "node-loader": "2.0.0",
"node-polyfill-webpack-plugin": "^2.0.1", "node-polyfill-webpack-plugin": "^2.0.1",
"noflo": "^1.4.3", "noflo": "^1.4.3",
@ -221,7 +223,8 @@
"fbp-graph": "$fbp-graph" "fbp-graph": "$fbp-graph"
}, },
"patchedDependencies": { "patchedDependencies": {
"the-graph@0.13.1": "patches/the-graph@0.13.1.patch" "the-graph@0.13.1": "patches/the-graph@0.13.1.patch",
"klayjs-noflo@0.3.1": "patches/klayjs-noflo@0.3.1.patch"
} }
}, },
"private": false "private": false

View file

@ -0,0 +1,22 @@
diff --git a/klay-noflo.js b/klay-noflo.js
index 80ec56bf689e9d35c7a4af4d8dd3d42392287228..58f438ce8eeb0106296e4554f0e2b938b29709ab 100644
--- a/klay-noflo.js
+++ b/klay-noflo.js
@@ -1,4 +1,4 @@
-var klayNoflo = (function () {
+export const klayNoflo = (function () {
"use strict";
var worker;
diff --git a/package.json b/package.json
index e78946c6141f2245e744976ba4a0bd3581057976..053d8a4b20e53a41871fd74ae26c778479eea762 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"dependencies": {
"klayjs": "^0.2.1"
},
+ "type": "module",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-clean": "~0.5.0",

18
pnpm-lock.yaml generated
View file

@ -4,6 +4,9 @@ overrides:
fbp-graph: ^0.7.0 fbp-graph: ^0.7.0
patchedDependencies: patchedDependencies:
klayjs-noflo@0.3.1:
hash: 6fjyliev4jv57qjijqsznbr3si
path: patches/klayjs-noflo@0.3.1.patch
the-graph@0.13.1: the-graph@0.13.1:
hash: 7l2bhm362kxjhsgmmhtyhvw7ga hash: 7l2bhm362kxjhsgmmhtyhvw7ga
path: patches/the-graph@0.13.1.patch path: patches/the-graph@0.13.1.patch
@ -469,6 +472,12 @@ devDependencies:
json5: json5:
specifier: ^2.2.3 specifier: ^2.2.3
version: 2.2.3 version: 2.2.3
klayjs:
specifier: 0.2.1
version: 0.2.1
klayjs-noflo:
specifier: ^0.3.1
version: 0.3.1(patch_hash=6fjyliev4jv57qjijqsznbr3si)
node-loader: node-loader:
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0(webpack@5.88.1) version: 2.0.0(webpack@5.88.1)
@ -9818,14 +9827,15 @@ packages:
engines: {node: '>=14.14.0'} engines: {node: '>=14.14.0'}
dev: true dev: true
/klayjs-noflo@0.3.1: /klayjs-noflo@0.3.1(patch_hash=6fjyliev4jv57qjijqsznbr3si):
resolution: {integrity: sha1-CS/lXMKJgFWTUDveUAG2pe3bSV8=} resolution: {integrity: sha512-sJJAPXahzVBrNIvLBx4mgn32Wf6XMx7ebaaBuaARnD7k3Ifpi3hTI5mqdBbyv5v/bq0jj6oqN3yo9kUkPxCv6g==}
dependencies: dependencies:
klayjs: 0.2.1 klayjs: 0.2.1
dev: true dev: true
patched: true
/klayjs@0.2.1: /klayjs@0.2.1:
resolution: {integrity: sha1-rLDvCmB8C86rAuhQGkK3WzFQZyA=} resolution: {integrity: sha512-cIW1NIuHqVy/yROTBNPs6/FjddGbLYVhECY6RaBPFVSxaHT6LRiVQZUr5qBYMySRZMJESVBWb4O33nE8lNGzbg==}
dev: true dev: true
/knuth-shuffle-seeded@1.0.6: /knuth-shuffle-seeded@1.0.6:
@ -13436,7 +13446,7 @@ packages:
fbp-graph: 0.7.0 fbp-graph: 0.7.0
font-awesome: 4.7.0 font-awesome: 4.7.0
hammerjs: 2.0.8 hammerjs: 2.0.8
klayjs-noflo: 0.3.1 klayjs-noflo: 0.3.1(patch_hash=6fjyliev4jv57qjijqsznbr3si)
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
tv4: 1.3.0 tv4: 1.3.0

View file

@ -1,4 +1,5 @@
import { sidebarWidth } from '@/constants/style'; import { sidebarWidth } from '@/constants/style';
import AlignHorizontalLeftIcon from '@mui/icons-material/AlignHorizontalLeft';
import EditOnIcon from '@mui/icons-material/Edit'; import EditOnIcon from '@mui/icons-material/Edit';
import EditLocationAltIcon from '@mui/icons-material/EditLocationAlt'; import EditLocationAltIcon from '@mui/icons-material/EditLocationAlt';
import EditOffIcon from '@mui/icons-material/EditOff'; import EditOffIcon from '@mui/icons-material/EditOff';
@ -10,15 +11,18 @@ import { IconButton, Toolbar, Tooltip } from '@mui/material';
import { PageType } from '@services/pages/interface'; import { PageType } from '@services/pages/interface';
import { WindowNames } from '@services/windows/WindowProperties'; import { WindowNames } from '@services/windows/WindowProperties';
import { useWorkspacesListObservable } from '@services/workspaces/hooks'; import { useWorkspacesListObservable } from '@services/workspaces/hooks';
import React, { Dispatch, MutableRefObject, SetStateAction, useCallback, useState } from 'react'; import type { Graph } from 'fbp-graph';
import React, { Dispatch, MutableRefObject, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import styled from 'styled-components'; import styled from 'styled-components';
import { ITheGraphEditor } from 'the-graph'; import type { ITheGraphEditor } from 'the-graph';
import autoLayout from 'the-graph/the-graph/the-graph-autolayout';
import { useLocation } from 'wouter'; import { useLocation } from 'wouter';
import { IWorkflowContext } from '../../useContext'; import { IWorkflowContext } from '../../useContext';
import { AddItemDialog } from '../../WorkflowManage/AddItemDialog'; import { AddItemDialog } from '../../WorkflowManage/AddItemDialog';
import { addWorkflowToWiki, useAvailableFilterTags } from '../../WorkflowManage/useWorkflowDataSource'; import { addWorkflowToWiki, useAvailableFilterTags } from '../../WorkflowManage/useWorkflowDataSource';
import { IWorkflowListItem } from '../../WorkflowManage/WorkflowList'; import { IWorkflowListItem } from '../../WorkflowManage/WorkflowList';
import { klayNoflo } from 'klayjs-noflo/klay-noflo';
import { searchBarWidth } from './styleConstant'; import { searchBarWidth } from './styleConstant';
const ToolbarContainer = styled(Toolbar)` const ToolbarContainer = styled(Toolbar)`
@ -43,12 +47,13 @@ const ToolbarContainer = styled(Toolbar)`
interface IGraphTopToolbarProps { interface IGraphTopToolbarProps {
editorReference: MutableRefObject<ITheGraphEditor | undefined>; editorReference: MutableRefObject<ITheGraphEditor | undefined>;
graph: Graph;
readonly: boolean; readonly: boolean;
setReadonly: Dispatch<SetStateAction<boolean>>; setReadonly: Dispatch<SetStateAction<boolean>>;
workflowContext: IWorkflowContext; workflowContext: IWorkflowContext;
} }
export const GraphTopToolbar = (props: IGraphTopToolbarProps) => { export const GraphTopToolbar = (props: IGraphTopToolbarProps) => {
const { editorReference, readonly, setReadonly, workflowContext } = props; const { editorReference, readonly, setReadonly, workflowContext, graph } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const [, setLocation] = useLocation(); const [, setLocation] = useLocation();
@ -69,6 +74,45 @@ export const GraphTopToolbar = (props: IGraphTopToolbarProps) => {
editorReference?.current?.triggerFit(); editorReference?.current?.triggerFit();
}, [editorReference]); }, [editorReference]);
const onAutoLayoutSuccess = useCallback((keilerGraph: unknown) => {
graph.startTransaction('autolayout');
autoLayout.applyToGraph(graph, keilerGraph, { snap: 36 });
graph.endTransaction('autolayout');
// Fit to window
zoomToFit();
}, [graph, zoomToFit]);
const autoLayouterReference = useRef<typeof klayNoflo | undefined>();
useEffect(() => {
const newAutoLayouter = klayNoflo.init({
onSuccess: onAutoLayoutSuccess,
workerScript: 'webWorkers/klayjs/klay.js',
});
autoLayouterReference.current = newAutoLayouter;
}, [onAutoLayoutSuccess]);
const applyAutolayout = useCallback(() => {
const portInfo = editorReference?.current?.refs?.graph?.portInfo;
// Calls the autolayouter
autoLayouterReference.current?.layout({
graph,
portInfo,
direction: 'RIGHT',
options: {
intCoordinates: true,
algorithm: 'de.cau.cs.kieler.klay.layered',
layoutHierarchy: true,
spacing: 36,
borderSpacing: 20,
edgeSpacingFactor: 0.2,
inLayerSpacingFactor: 2,
nodePlace: 'BRANDES_KOEPF',
nodeLayering: 'NETWORK_SIMPLEX',
edgeRouting: 'POLYLINE',
crossMin: 'LAYER_SWEEP',
direction: 'RIGHT',
},
});
}, [editorReference, graph]);
const [changeGraphInfoDialogOpen, setChangeGraphInfoDialogOpen] = useState(false); const [changeGraphInfoDialogOpen, setChangeGraphInfoDialogOpen] = useState(false);
const workspacesList = useWorkspacesListObservable(); const workspacesList = useWorkspacesListObservable();
const [availableFilterTags] = useAvailableFilterTags(workspacesList); const [availableFilterTags] = useAvailableFilterTags(workspacesList);
@ -111,6 +155,11 @@ export const GraphTopToolbar = (props: IGraphTopToolbarProps) => {
<ZoomOutMapIcon /> <ZoomOutMapIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title={t('Workflow.AutoLayout')}>
<IconButton onClick={applyAutolayout}>
<AlignHorizontalLeftIcon />
</IconButton>
</Tooltip>
<Tooltip title={t('Workflow.ChangeWorkflowMetadata')}> <Tooltip title={t('Workflow.ChangeWorkflowMetadata')}>
<IconButton onClick={changeGraphInfo}> <IconButton onClick={changeGraphInfo}>
<InfoIcon /> <InfoIcon />

View file

@ -65,9 +65,9 @@ export function GraphEditor() {
onEdgeSelection, onEdgeSelection,
onNodeSelection, onNodeSelection,
onPanScale, onPanScale,
// triggerAutolayout,
// applyAutolayout,
addNode, addNode,
selectedNodes,
selectedEdges,
} = useMouseEvents({ } = useMouseEvents({
graph, graph,
library, library,
@ -109,7 +109,7 @@ export function GraphEditor() {
/> />
</ThumbnailContainer> </ThumbnailContainer>
<SearchComponents library={library} addNode={addNode} /> <SearchComponents library={library} addNode={addNode} />
<GraphTopToolbar editorReference={editorReference} readonly={readonly} setReadonly={setReadonly} workflowContext={workflowContext} /> <GraphTopToolbar editorReference={editorReference} readonly={readonly} setReadonly={setReadonly} workflowContext={workflowContext} graph={graph} />
</ErrorBoundary> </ErrorBoundary>
); );
} }

View file

@ -16,9 +16,6 @@ const unnamespace = (name: string) => {
export function useMouseEvents({ graph, library, setGraph }: { graph?: Graph; library?: IFBPLibrary; setGraph: (graph: Graph) => void }) { export function useMouseEvents({ graph, library, setGraph }: { graph?: Graph; library?: IFBPLibrary; setGraph: (graph: Graph) => void }) {
const [selectedNodes, setSelectedNodes] = useState<GraphNode[]>([]); const [selectedNodes, setSelectedNodes] = useState<GraphNode[]>([]);
const [selectedEdges, setSelectedEdges] = useState<GraphEdge[]>([]); const [selectedEdges, setSelectedEdges] = useState<GraphEdge[]>([]);
const [_, triggerForceRerender] = useToggle();
const [icons, setIcons] = useState<any[]>([]);
const [pan, setPan] = useState<[number, number]>([0, 0]); const [pan, setPan] = useState<[number, number]>([0, 0]);
const [scale, setScale] = useState<number>(1); const [scale, setScale] = useState<number>(1);
@ -95,5 +92,7 @@ export function useMouseEvents({ graph, library, setGraph }: { graph?: Graph; li
onNodeSelection, onNodeSelection,
onPanScale, onPanScale,
addNode, addNode,
selectedNodes,
selectedEdges,
}; };
} }

113
src/type.d.ts vendored
View file

@ -71,6 +71,9 @@ declare module 'the-graph' {
library: IFBPLibrary; library: IFBPLibrary;
libraryDirty: boolean; libraryDirty: boolean;
pinching: boolean; pinching: boolean;
refs: {
graph?: ITheGraphEditorGraph;
};
registerComponent: (definition: NoFloComponent, generated: boolean) => void; registerComponent: (definition: NoFloComponent, generated: boolean) => void;
/** /**
* This is undefined, because it is in the-graph/the-graph-graph.js * This is undefined, because it is in the-graph/the-graph-graph.js
@ -95,6 +98,62 @@ declare module 'the-graph' {
zoomX: number; zoomX: number;
zoomY: number; zoomY: number;
} }
export interface ITheGraphEditorGraphState {
animatedEdges: GraphEdge[];
displaySelectionGroup: boolean;
edgePreview: GraphEdge | null;
edgePreviewX: number;
edgePreviewY: number;
errorNodes: GraphNode[];
forceSelection: boolean;
offsetX: number;
offsetY: number;
selectedEdges: GraphEdge[];
selectedNodes: GraphNode[];
}
export interface ITheGraphEditorGraphProps {
app: ITheGraphEditor | null;
graph: Graph;
library: IFBPLibrary;
// allows overriding icon of a node
nodeIcons: Record<string, string>;
offsetX: number;
offsetY: number;
}
export interface ITheGraphEditorGraph {
addEdge: Function;
cancelPreviewEdge: Function;
context: {};
dirty: false;
edgeStart: Function;
getComponentInfo: Function;
getGraphInport: Function;
getGraphOutport: Function;
getNodeInport: Function;
getNodeOutport: Function;
getPorts: Function;
markDirty: Function;
mounted: true;
moveGroup: Function;
/**
* ```json
* {"adapters/ObjectToString_emfdv":{"inports":{"in":{"label":"in","type":"object","x":0,"y":18},"assoc":{"label":"assoc","type":"string","x":0,"y":36,"route":0},"delim":{"label":"delim","type":"string","x":0,"y":54,"route":0}},"outports":{"out":{"label":"out","type":"string","x":72,"y":36,"route":0}}},"adapters/PacketsToObject_llf0k":{"inports":{"in":{"label":"in","type":"all","x":0,"y":36,"route":0}},"outports":{"out":{"label":"out","type":"object","x":72,"y":36}}}}
* ```
*/
portInfo?: Record<string, { inports: INoFloProtocolComponentPort[]; outports: INoFloProtocolComponentPort[] }>;
props: ITheGraphEditorGraphProps;
refs: {};
renderPreviewEdge: Function;
resetPortRoute: Function;
setAnimatedEdges: Function;
setErrorNodes: Function;
setSelectedEdges: Function;
setSelectedNodes: Function;
state: ITheGraphEditorGraphState;
subscribeGraph: Function;
triggerRender: Function;
updateIcon: Function;
}
export function App(props: ITheGraphProps): JSX.Element; export function App(props: ITheGraphProps): JSX.Element;
export interface INoFloProtocolComponentPort { export interface INoFloProtocolComponentPort {
@ -232,12 +291,66 @@ declare module 'the-graph' {
``` ```
*/ */
export const FONT_AWESOME: Record<string, string>; export const FONT_AWESOME: Record<string, string>;
/**
*
* @param keilerGraph assign by
* ```js
* autolayouter = klayNoflo.init({
onSuccess: this.applyAutolayout.bind(this),
workerScript: 'vendor/klayjs/klay.js',
})
```
* @param props `{ snap: 36 }` in noflo-ui example.
*/
function applyAutolayout(graph: Graph, keilerGraph, props: { snap: number }): void;
export const autolayout = {
applyToGraph: applyAutolayout,
};
} }
declare module 'the-graph/the-graph-nav/the-graph-nav' { declare module 'the-graph/the-graph-nav/the-graph-nav' {
import { nav } from 'the-graph'; import { nav } from 'the-graph';
export const Component = nav.Component; export const Component = nav.Component;
} }
declare module 'the-graph/the-graph/the-graph-autolayout' {
import { autolayout } from 'the-graph';
// eslint-disable-next-line unicorn/prefer-export-from
export default autolayout;
}
declare module 'klayjs-noflo/klay-noflo' {
import { Graph } from 'fbp-graph';
export interface IKlayLayoutOptions {
direction: string;
graph: Graph;
options: {
algorithm: string;
borderSpacing: number;
crossMin: string;
direction: string;
edgeRouting: string;
edgeSpacingFactor: number;
inLayerSpacingFactor: number;
intCoordinates: boolean;
layoutHierarchy: boolean;
nodeLayering: string;
nodePlace: string;
spacing: number;
};
portInfo:
| Record<string, {
inports: INoFloProtocolComponentPort[];
outports: INoFloProtocolComponentPort[];
}>
| undefined;
}
export const klayNoflo: {
init(options: { onSuccess: (keilerGraph: unknown) => void; workerScript: string }): typeof klayNoflo;
layout(options: IKlayLayoutOptions): void;
};
}
declare module 'espree' { declare module 'espree' {
// https://github.com/eslint/espree#options // https://github.com/eslint/espree#options
export interface Options { export interface Options {

View file

@ -84,4 +84,13 @@ exports.renderer = _.compact([
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
? new BundleAnalyzerPlugin({ generateStatsFile: true, analyzerMode: 'disabled', statsFilename: '../../out/webpack-stats-renderer.json' }) ? new BundleAnalyzerPlugin({ generateStatsFile: true, analyzerMode: 'disabled', statsFilename: '../../out/webpack-stats-renderer.json' })
: undefined, : undefined,
new CopyPlugin({
patterns: [
// similar to noflo-ui's webpack.config.js
{
from: 'node_modules/klayjs/klay.js',
to: 'webWorkers/klayjs/klay.js',
},
],
}),
]); ]);