Compare commits
19 Commits
v2.0.6-bet
...
v2.0.6-bet
Author | SHA1 | Date | |
---|---|---|---|
12e4ac55d7 | |||
294672b306 | |||
68c2346e0c | |||
24de1254a1 | |||
88918f7611 | |||
5e102a39c5 | |||
2f310fcceb | |||
40d53355ce | |||
1dcb3d92fd | |||
9fe7cd51a3 | |||
34481de1db | |||
9d597a92da | |||
40f278f064 | |||
f3edbbc95d | |||
50a5dc7cd8 | |||
5aeae976cd | |||
9200e03f52 | |||
6f089f57a8 | |||
48def793ed |
@ -11,11 +11,6 @@ coverage:
|
||||
threshold: 1%
|
||||
flags:
|
||||
- x6
|
||||
x6-vector:
|
||||
threshold: 1%
|
||||
target: 80% # will fail a Pull Request if coverage is less than 80%
|
||||
flags:
|
||||
- x6-vector
|
||||
x6-geometry:
|
||||
threshold: 1%
|
||||
flags:
|
||||
@ -26,9 +21,6 @@ flags:
|
||||
paths:
|
||||
# filter the folder(s) you wish to measure by that flag
|
||||
- packages/x6
|
||||
x6-vector:
|
||||
paths:
|
||||
- packages/x6-vector
|
||||
x6-geometry:
|
||||
paths:
|
||||
- packages/x6-geometry
|
||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -54,12 +54,6 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/x6/test/coverage/lcov.info
|
||||
flags: x6
|
||||
- name: 💡 Codecov(x6-vector)
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/x6-vector/test/coverage/lcov.info
|
||||
flags: x6-vector
|
||||
- name: 💡 Codecov(x6-geometry)
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
|
@ -1,16 +0,0 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "eslint-config-umi"
|
||||
}
|
20
examples/x6-app-dag/.gitignore
vendored
20
examples/x6-app-dag/.gitignore
vendored
@ -1,20 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/npm-debug.log*
|
||||
/yarn-error.log
|
||||
/yarn.lock
|
||||
/package-lock.json
|
||||
|
||||
# production
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
# umi
|
||||
/src/.umi
|
||||
/src/.umi-production
|
||||
/src/.umi-test
|
||||
/.env.local
|
@ -1,8 +0,0 @@
|
||||
**/*.md
|
||||
**/*.svg
|
||||
**/*.ejs
|
||||
**/*.html
|
||||
package.json
|
||||
.umi
|
||||
.umi-production
|
||||
.umi-test
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"semi": false,
|
||||
"printWidth": 80,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ".prettierrc",
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import { defineConfig } from 'umi'
|
||||
|
||||
export default defineConfig({
|
||||
publicPath: './',
|
||||
routes: [
|
||||
{ path: '/', component: '@/pages/index' },
|
||||
{ path: '/apps/dag', component: '@/pages/index' },
|
||||
],
|
||||
theme: {
|
||||
'@ant-prefix': 'ant',
|
||||
'@menu-item-active-bg': '#f0f5ff',
|
||||
},
|
||||
extraBabelPlugins: [
|
||||
[
|
||||
'import',
|
||||
{
|
||||
libraryName: '@antv/x6-react-components',
|
||||
libraryDirectory: 'es',
|
||||
transformToDefaultImport: false,
|
||||
style: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
})
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +0,0 @@
|
||||
# X6 DAG React demo project
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install dependencies,
|
||||
|
||||
```bash
|
||||
$ yarn
|
||||
```
|
||||
|
||||
Start the dev server,
|
||||
|
||||
```bash
|
||||
$ yarn start
|
||||
```
|
@ -1,49 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@antv/x6-app-dag",
|
||||
"version": "1.1.7",
|
||||
"scripts": {
|
||||
"start": "umi dev",
|
||||
"build": "umi build",
|
||||
"postinstall": "umi generate tmp",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"lint": "umi-lint --eslint src/ -p.no-semi --prettier --fix",
|
||||
"test:coverage": "umi-test --coverage"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,less,md,json}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.ts?(x)": [
|
||||
"prettier --parser=typescript --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.2.1",
|
||||
"@antv/x6": "^1.30.2",
|
||||
"@antv/x6-react-components": "^1.1.16",
|
||||
"@antv/x6-react-shape": "^1.6.0",
|
||||
"@types/dompurify": "^2.0.4",
|
||||
"ahooks": "^2.7.0",
|
||||
"antd": "^4.4.2",
|
||||
"classnames": "^2.2.6",
|
||||
"dompurify": "^2.1.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dnd": "^11.1.3",
|
||||
"react-dnd-html5-backend": "^11.1.3",
|
||||
"react-dom": "^16.13.1",
|
||||
"umi-lint": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/pro-layout": "^5.0.12",
|
||||
"@umijs/preset-react": "1.x",
|
||||
"@umijs/test": "^3.2.19",
|
||||
"lint-staged": "^10.5.3",
|
||||
"prettier": "^2.2.1",
|
||||
"umi": "^3.2.19",
|
||||
"yorkie": "^2.0.0"
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
|
||||
export const useObservableState = <T extends any>(
|
||||
source$: Observable<T> | { (): Observable<T> },
|
||||
initialState?: T,
|
||||
): [T, React.Dispatch<React.SetStateAction<T>>] => {
|
||||
const source = useMemo<Observable<T>>(() => {
|
||||
if (typeof source$ === 'function') {
|
||||
return source$()
|
||||
}
|
||||
return source$
|
||||
}, [source$])
|
||||
const [state, setState] = useState<T>(() => {
|
||||
if (source instanceof BehaviorSubject) {
|
||||
return source.getValue()
|
||||
}
|
||||
return initialState
|
||||
})
|
||||
useEffect(() => {
|
||||
const sub = source.subscribe((v) => {
|
||||
setState(v)
|
||||
})
|
||||
return () => {
|
||||
sub.unsubscribe()
|
||||
}
|
||||
}, [source])
|
||||
return [state, setState]
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
export { unescape } from 'lodash-es'
|
||||
|
||||
export class Deferred<T> {
|
||||
resolve!: (value?: T) => void
|
||||
|
||||
reject!: (err?: any) => void
|
||||
|
||||
promise: Promise<T>
|
||||
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 JSON 字符串不引起报错
|
||||
export const safeJson = (jsonStr = '{}', defaultVal = {}) => {
|
||||
try {
|
||||
return JSON.parse(jsonStr)
|
||||
} catch (error) {
|
||||
console.warn(`${jsonStr} is not valid json`)
|
||||
return defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
export class CodeName {
|
||||
static parse(codeName = '') {
|
||||
return codeName.replace(/_\d+$/, '').toLocaleLowerCase()
|
||||
}
|
||||
|
||||
static equal(c1: string, c2: string) {
|
||||
return CodeName.parse(c1) === CodeName.parse(c2)
|
||||
}
|
||||
|
||||
static some(list: string[], c2: string) {
|
||||
return list.some((c1) => CodeName.equal(c1, c2))
|
||||
}
|
||||
|
||||
static getFromNode(node: any = {}) {
|
||||
const { codeName = '' } = node
|
||||
return CodeName.parse(codeName)
|
||||
}
|
||||
}
|
||||
|
||||
export const isPromise = (obj: any) =>
|
||||
!!obj &&
|
||||
(typeof obj === 'object' || typeof obj === 'function') &&
|
||||
typeof obj.then === 'function'
|
@ -1,6 +0,0 @@
|
||||
.no-wrap {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import styles from './cut.less'
|
||||
|
||||
interface Props {
|
||||
left: number
|
||||
right: number
|
||||
max: number
|
||||
children: string
|
||||
}
|
||||
|
||||
export const Cut: React.FC<Props> = (props) => {
|
||||
const { left, right = 0, max, children } = props
|
||||
const getText = useCallback(() => {
|
||||
const len = children.length
|
||||
const ellipsis = '...'
|
||||
let leftStr = ''
|
||||
let rightStr = ''
|
||||
|
||||
if (len > max) {
|
||||
if (left && len) {
|
||||
leftStr = children.substr(0, left)
|
||||
} else {
|
||||
leftStr = children.substr(0, max)
|
||||
}
|
||||
|
||||
if (right) {
|
||||
rightStr = children.substr(-right, right)
|
||||
}
|
||||
|
||||
return `${leftStr}${ellipsis}${rightStr}`
|
||||
}
|
||||
|
||||
return children
|
||||
}, [left, right, max, children])
|
||||
return <span className={styles['no-wrap']}>{getText()}</span>
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import React from 'react'
|
||||
import { unescape } from 'lodash-es'
|
||||
import { Cut } from '@/component/cut'
|
||||
import { Keyword } from '@/component/keyword'
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
|
||||
export const ItemName: React.FC<Props> = (props) => {
|
||||
const { data } = props
|
||||
const { keyword, cutParas = {} } = data
|
||||
const name = unescape(data.name)
|
||||
const { max, side } = cutParas
|
||||
if (keyword) {
|
||||
return <Keyword raw={name} keyword={keyword} />
|
||||
}
|
||||
if (max) {
|
||||
return (
|
||||
<Cut max={max} left={side} right={side}>
|
||||
{name}
|
||||
</Cut>
|
||||
)
|
||||
}
|
||||
return <span>{name}</span>
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
.keywordWrapper {
|
||||
strong {
|
||||
color: #dd4b39;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import styles from './keyword.less'
|
||||
|
||||
interface Props {
|
||||
raw: string
|
||||
keyword: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Keyword: React.FC<Props> = (props) => {
|
||||
const { raw, keyword, className } = props
|
||||
if (keyword) {
|
||||
const regex = new RegExp(keyword, 'ig')
|
||||
const arr = raw.split(regex)
|
||||
return (
|
||||
<span
|
||||
className={classnames({
|
||||
[styles.keywordWrapper]: true,
|
||||
[className!]: !!className,
|
||||
})}
|
||||
>
|
||||
{arr.map((section, index) =>
|
||||
index !== arr.length - 1 ? (
|
||||
<span key={section + index}>
|
||||
{section}
|
||||
<strong>{keyword}</strong>
|
||||
</span>
|
||||
) : (
|
||||
section
|
||||
),
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Modal, ConfigProvider } from 'antd'
|
||||
import { ModalFuncProps, ModalProps } from 'antd/es/modal'
|
||||
import { isPromise } from '@/common/utils'
|
||||
import { ANT_PREFIX } from '@/constants/global'
|
||||
|
||||
type ShowProps = ModalProps & {
|
||||
afterClose?: (...args: any[]) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const showModal = (props: ShowProps) => {
|
||||
const div = document.createElement('div')
|
||||
document.body.appendChild(div)
|
||||
|
||||
let config: ShowProps = {
|
||||
...props,
|
||||
visible: true,
|
||||
onCancel: close,
|
||||
onOk: (e) => {
|
||||
if (typeof props.onOk === 'function') {
|
||||
const ret = props.onOk(e)
|
||||
if (isPromise(ret as any)) {
|
||||
;(ret as any).then(() => {
|
||||
close()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function destroy(...args: any[]) {
|
||||
const unmountResult = ReactDOM.unmountComponentAtNode(div)
|
||||
if (unmountResult && div.parentNode) {
|
||||
div.parentNode.removeChild(div)
|
||||
}
|
||||
if (typeof props.afterClose === 'function') {
|
||||
props.afterClose(...args)
|
||||
}
|
||||
}
|
||||
|
||||
function update(newConfig: ModalFuncProps) {
|
||||
config = {
|
||||
...config,
|
||||
...newConfig,
|
||||
}
|
||||
render(config)
|
||||
}
|
||||
|
||||
function close(...args: any[]) {
|
||||
const nextConfig = {
|
||||
...config,
|
||||
visible: false,
|
||||
afterClose: destroy.bind(undefined, ...args),
|
||||
}
|
||||
update(nextConfig)
|
||||
}
|
||||
|
||||
function render(usedConfig: ModalProps & { children: React.ReactNode }) {
|
||||
const { children, ...others } = usedConfig
|
||||
setTimeout(() => {
|
||||
ReactDOM.render(
|
||||
<ConfigProvider prefixCls={ANT_PREFIX}>
|
||||
<Modal {...others}>{children}</Modal>
|
||||
</ConfigProvider>,
|
||||
div,
|
||||
)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
render(config)
|
||||
|
||||
return {
|
||||
close,
|
||||
update,
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Observable } from 'rxjs'
|
||||
import { Input, ConfigProvider } from 'antd'
|
||||
import { InputProps } from 'antd/es/input'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { ANT_PREFIX } from '@/constants/global'
|
||||
|
||||
interface RxInputProps extends Omit<InputProps, 'value'> {
|
||||
value: Observable<string>
|
||||
}
|
||||
|
||||
export const RxInput: React.FC<RxInputProps> = (props) => {
|
||||
const { value, ...otherProps } = props
|
||||
const [realValue] = useObservableState(() => value)
|
||||
return (
|
||||
<ConfigProvider prefixCls={ANT_PREFIX}>
|
||||
<Input value={realValue} {...otherProps} />
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
@ -1 +0,0 @@
|
||||
export const ANT_PREFIX = 'ant'
|
@ -1,14 +0,0 @@
|
||||
export const GROUP_HORIZONTAL__PADDING = 24 // 分组横向 padding
|
||||
export const GROUP_VERTICAL__PADDING = 40 // 分组纵向 padding
|
||||
export const NODE_WIDTH = 180
|
||||
export const NODE_HEIGHT = 32
|
||||
|
||||
// 触发画布重新渲染事件
|
||||
export const RERENDER_EVENT = 'RERENDER_EVENT'
|
||||
|
||||
/*
|
||||
* 以下是拖拽相关
|
||||
*/
|
||||
|
||||
export const DRAGGABLE_ALGO_COMPONENT = 'ALGO_COMPONENT'
|
||||
export const DRAGGABLE_MODEL = 'MODEL'
|
@ -1,42 +0,0 @@
|
||||
.menuWrap {
|
||||
max-height: 316px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
left: -1px;
|
||||
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.1);
|
||||
:global {
|
||||
.@{ant-prefix}-menu-item {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0px 8px;
|
||||
margin: 0 !important;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 13px;
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background-color: @menu-item-active-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
padding-right: 2px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
height: 48px;
|
||||
margin-left: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 12px;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import React from 'react'
|
||||
import css from './index.less'
|
||||
|
||||
export interface IProps {
|
||||
experimentName?: string
|
||||
}
|
||||
|
||||
export const ExperimentTitle: React.FC<IProps> = ({ experimentName }) => {
|
||||
return (
|
||||
<div className={css.wrap}>
|
||||
<span className={css.name}> {experimentName} </span>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
.header {
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
overflow: hidden;
|
||||
line-height: 48px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 0;
|
||||
.headerLeft,
|
||||
.headerRight {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.doc {
|
||||
margin-right: 32px;
|
||||
font-size: 12px;
|
||||
a {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 0 4px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
:global {
|
||||
.anticon {
|
||||
position: relative;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Layout } from 'antd'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { GithubOutlined } from '@ant-design/icons'
|
||||
import { SimpleLogo } from './logo'
|
||||
import { ExperimentTitle } from './experiment-title'
|
||||
|
||||
import css from './index.less'
|
||||
|
||||
const { Header } = Layout
|
||||
|
||||
interface IProps {
|
||||
experimentId: string
|
||||
}
|
||||
|
||||
export const GuideHeader: React.FC<IProps> = (props) => {
|
||||
const expGraph = useExperimentGraph(props.experimentId)
|
||||
const [activeExperiment] = useObservableState(expGraph.experiment$)
|
||||
|
||||
const openGithub = () => {
|
||||
window.open(
|
||||
'https://github.com/antvis/X6/tree/master/examples/x6-app-dag',
|
||||
'_blank',
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header className={css.header}>
|
||||
<div className={css.headerLeft}>
|
||||
<SimpleLogo />
|
||||
<ExperimentTitle experimentName={activeExperiment.name} />
|
||||
</div>
|
||||
<div className={css.headerRight}>
|
||||
<div className={css.doc}>
|
||||
<GithubOutlined onClick={openGithub} />
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
.root {
|
||||
height: 48px;
|
||||
width: 64px;
|
||||
line-height: 48px;
|
||||
position: relative;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
||||
color: #fba831;
|
||||
}
|
||||
|
||||
.logo {
|
||||
top: 8px;
|
||||
left: 18px;
|
||||
position: absolute;
|
||||
font-size: 28px;
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import { ApartmentOutlined } from '@ant-design/icons'
|
||||
import css from './index.less'
|
||||
|
||||
interface Props {
|
||||
border?: boolean
|
||||
}
|
||||
|
||||
export const SimpleLogo: React.FC<Props> = ({ border }) => {
|
||||
return (
|
||||
<div className={`${css.root} `}>
|
||||
<ApartmentOutlined className={css.logo} />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,268 +0,0 @@
|
||||
export const algoData = [
|
||||
{
|
||||
id: 'recentlyUsed',
|
||||
name: '最近使用',
|
||||
isDir: true,
|
||||
children: [
|
||||
{
|
||||
id: 10,
|
||||
defSource: 2,
|
||||
docUrl: '',
|
||||
ioType: 0,
|
||||
up: 148,
|
||||
down: 11,
|
||||
iconType: 1,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
codeName: 'algo_1',
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
engineType: 0,
|
||||
isComposite: false,
|
||||
sequence: 0,
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
name: '算法组件1',
|
||||
parentId: 'recentlyUsed',
|
||||
isBranch: false,
|
||||
social: {
|
||||
defSource: 2,
|
||||
isEnabled: true,
|
||||
docUrl: '#',
|
||||
iconType: 1,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
codeName: 'algo_1',
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
name: '算法组件1',
|
||||
id: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
defSource: 2,
|
||||
docUrl: '',
|
||||
ioType: 0,
|
||||
up: 148,
|
||||
down: 11,
|
||||
iconType: 1,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
codeName: 'algo_2',
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
engineType: 0,
|
||||
isComposite: false,
|
||||
sequence: 0,
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
name: '算法组件2',
|
||||
parentId: 'recentlyUsed',
|
||||
isBranch: false,
|
||||
social: {
|
||||
defSource: 2,
|
||||
isEnabled: true,
|
||||
docUrl: '#',
|
||||
iconType: 1,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
codeName: 'algo_2',
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
name: '算法组件2',
|
||||
id: 11,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
defSource: 2,
|
||||
docUrl: '',
|
||||
ioType: 0,
|
||||
up: 148,
|
||||
down: 11,
|
||||
iconType: 1,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
codeName: 'algo_3',
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
engineType: 0,
|
||||
isComposite: false,
|
||||
sequence: 0,
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
name: '算法组件3',
|
||||
parentId: 'recentlyUsed',
|
||||
isBranch: false,
|
||||
social: {
|
||||
defSource: 2,
|
||||
isEnabled: true,
|
||||
docUrl: '#',
|
||||
iconType: 1,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
codeName: 'algo_3',
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
name: '算法组件3',
|
||||
id: 12,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '数据读写',
|
||||
id: 21,
|
||||
category: 'source',
|
||||
isDir: true,
|
||||
children: [
|
||||
{
|
||||
defSource: 2,
|
||||
docUrl: '',
|
||||
ioType: 0,
|
||||
up: 148,
|
||||
down: 11,
|
||||
iconType: 1,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
codeName: 'odps_source',
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
engineType: 0,
|
||||
isComposite: false,
|
||||
sequence: 0,
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
name: '读数据表',
|
||||
id: 100,
|
||||
parentId: 'recentlyUsed',
|
||||
isBranch: false,
|
||||
social: {
|
||||
defSource: 2,
|
||||
isEnabled: true,
|
||||
docUrl: '#',
|
||||
iconType: 1,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
codeName: 'odps_source',
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
name: '读数据表',
|
||||
id: 100,
|
||||
},
|
||||
},
|
||||
],
|
||||
isBranch: true,
|
||||
isExpanded: false,
|
||||
codeName: 'source',
|
||||
parentId: 'platformAlgo',
|
||||
},
|
||||
{
|
||||
name: '统计分析',
|
||||
id: 22,
|
||||
category: 'analytics',
|
||||
isDir: true,
|
||||
children: [],
|
||||
isBranch: true,
|
||||
isExpanded: false,
|
||||
codeName: 'analytics',
|
||||
parentId: 'platformAlgo',
|
||||
},
|
||||
{
|
||||
name: '算法',
|
||||
id: 23,
|
||||
category: 'ai_algo',
|
||||
isDir: true,
|
||||
children: [],
|
||||
isBranch: true,
|
||||
isExpanded: false,
|
||||
codeName: 'algorithm',
|
||||
parentId: 'platformAlgo',
|
||||
},
|
||||
{
|
||||
name: '预测',
|
||||
id: 24,
|
||||
category: 'predict',
|
||||
isDir: true,
|
||||
children: [],
|
||||
isBranch: true,
|
||||
isExpanded: false,
|
||||
codeName: 'predict',
|
||||
parentId: 'platformAlgo',
|
||||
},
|
||||
{
|
||||
name: '评估',
|
||||
id: 25,
|
||||
category: 'evaluation',
|
||||
isDir: true,
|
||||
children: [],
|
||||
isBranch: true,
|
||||
isExpanded: false,
|
||||
codeName: 'evaluation',
|
||||
parentId: 'platformAlgo',
|
||||
},
|
||||
]
|
||||
|
||||
export const searchByKeyword = async (keyword: string) => {
|
||||
return Array(10)
|
||||
.fill(null)
|
||||
.map((i, idx) => {
|
||||
return {
|
||||
defSource: 2,
|
||||
docUrl: '',
|
||||
ioType: 0,
|
||||
up: 148,
|
||||
down: 11,
|
||||
iconType: 1,
|
||||
isDir: false,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
codeName: `${keyword}${idx}`,
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
engineType: 0,
|
||||
isComposite: false,
|
||||
sequence: 0,
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
name: `${keyword}__${idx}`,
|
||||
id: idx,
|
||||
parentId: 'recentlyUsed',
|
||||
isBranch: false,
|
||||
social: {
|
||||
defSource: 2,
|
||||
isEnabled: true,
|
||||
docUrl: '#',
|
||||
iconType: 1,
|
||||
isDisabled: false,
|
||||
author: 'demo author',
|
||||
name: `${keyword}-${idx}`,
|
||||
codeName: `${keyword}${idx}`,
|
||||
catId: 1,
|
||||
lastModifyTime: '2020-08-25 15:43:39',
|
||||
createdTime: '2015-04-16 13:38:11',
|
||||
owner: 'system',
|
||||
description: '组件描述信息',
|
||||
id: idx,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
@ -1,720 +0,0 @@
|
||||
import random from 'lodash/random'
|
||||
|
||||
interface NodeParams {
|
||||
name: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export const copyNode = ({ name, x, y }: NodeParams) => {
|
||||
const id = `${Date.now()}`
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: id + 100000,
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: id + 200000,
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: id + 300000,
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: id + 400000,
|
||||
},
|
||||
],
|
||||
positionX: x + 200 + random(20, false),
|
||||
positionY: y + random(10, false),
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
}
|
||||
}
|
||||
export const addNode = ({ name, x, y }: NodeParams) => {
|
||||
const id = `${Date.now()}`
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: id + '_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: id + '_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: id + '_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: id + '_out_2',
|
||||
},
|
||||
],
|
||||
positionX: x,
|
||||
positionY: y,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const queryGraph = (id: string) => {
|
||||
return {
|
||||
lang: 'zh_CN',
|
||||
success: true,
|
||||
data: initData,
|
||||
Lang: 'zh_CN',
|
||||
}
|
||||
}
|
||||
|
||||
export const addNodeGroup = async (groupName: string) => {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
group: {
|
||||
name: groupName,
|
||||
id: Date.now(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const initData = {
|
||||
nodes: [
|
||||
{
|
||||
id: '1603716783816',
|
||||
name: '算法组件1',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716783816_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716783816_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716783816_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716783816_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -200,
|
||||
positionY: -300,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716786205',
|
||||
name: '算法组件2',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716786205_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716786205_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716786205_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716786205_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -369,
|
||||
positionY: -161,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716788394',
|
||||
name: '算法组件2',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716788394_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716788394_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716788394_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716788394_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -154,
|
||||
positionY: -161,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716792826',
|
||||
name: '算法组件3',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716792826_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716792826_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716792826_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716792826_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -520,
|
||||
positionY: -30,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716795011',
|
||||
name: '算法组件2',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716795011_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716795011_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716795011_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716795011_out_2',
|
||||
},
|
||||
],
|
||||
positionX: 74,
|
||||
positionY: -160,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716814719',
|
||||
name: '算法组件3',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716814719_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716814719_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716814719_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716814719_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -310,
|
||||
positionY: -30,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716822805',
|
||||
name: '算法组件3',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716822805_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716822805_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716822805_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716822805_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -50,
|
||||
positionY: -30,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716828657',
|
||||
name: '算法组件3',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716828657_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716828657_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716828657_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716828657_out_2',
|
||||
},
|
||||
],
|
||||
positionX: 160,
|
||||
positionY: -30,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716834901',
|
||||
name: '算法组件2',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716834901_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716834901_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716834901_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716834901_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -390,
|
||||
positionY: 90,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716844054',
|
||||
name: '算法组件2',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716844054_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716844054_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716844054_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716844054_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -170,
|
||||
positionY: 90,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716854368',
|
||||
name: '算法组件2',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716854368_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716854368_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716854368_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716854368_out_2',
|
||||
},
|
||||
],
|
||||
positionX: 40,
|
||||
positionY: 90,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716858435',
|
||||
name: '算法组件3',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716858435_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716858435_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716858435_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716858435_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -310,
|
||||
positionY: 230,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
{
|
||||
id: '1603716868041',
|
||||
name: '算法组件2',
|
||||
inPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输入1',
|
||||
id: '1603716868041_in_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输入2',
|
||||
id: '1603716868041_in_2',
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 1,
|
||||
description: '输出表1',
|
||||
id: '1603716868041_out_1',
|
||||
},
|
||||
{
|
||||
tableName: 'germany_credit_data',
|
||||
sequence: 2,
|
||||
description: '输出表2',
|
||||
id: '1603716868041_out_2',
|
||||
},
|
||||
],
|
||||
positionX: -100,
|
||||
positionY: 230,
|
||||
codeName: 'source_11111',
|
||||
catId: 1,
|
||||
nodeDefId: 111111,
|
||||
category: 'source',
|
||||
status: 3,
|
||||
groupId: 0,
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
source: '1603716783816',
|
||||
target: '1603716786205',
|
||||
outputPortId: '1603716783816_out_1',
|
||||
inputPortId: '1603716786205_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716783816',
|
||||
target: '1603716788394',
|
||||
outputPortId: '1603716783816_out_2',
|
||||
inputPortId: '1603716788394_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716783816',
|
||||
target: '1603716795011',
|
||||
outputPortId: '1603716783816_out_2',
|
||||
inputPortId: '1603716795011_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716786205',
|
||||
target: '1603716792826',
|
||||
outputPortId: '1603716786205_out_1',
|
||||
inputPortId: '1603716792826_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716788394',
|
||||
target: '1603716814719',
|
||||
outputPortId: '1603716788394_out_1',
|
||||
inputPortId: '1603716814719_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716795011',
|
||||
target: '1603716822805',
|
||||
outputPortId: '1603716795011_out_1',
|
||||
inputPortId: '1603716822805_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716795011',
|
||||
target: '1603716828657',
|
||||
outputPortId: '1603716795011_out_2',
|
||||
inputPortId: '1603716828657_in_2',
|
||||
},
|
||||
{
|
||||
source: '1603716792826',
|
||||
target: '1603716834901',
|
||||
outputPortId: '1603716792826_out_1',
|
||||
inputPortId: '1603716834901_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716814719',
|
||||
target: '1603716844054',
|
||||
outputPortId: '1603716814719_out_1',
|
||||
inputPortId: '1603716844054_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716822805',
|
||||
target: '1603716854368',
|
||||
outputPortId: '1603716822805_out_1',
|
||||
inputPortId: '1603716854368_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716834901',
|
||||
target: '1603716858435',
|
||||
outputPortId: '1603716834901_out_1',
|
||||
inputPortId: '1603716858435_in_1',
|
||||
},
|
||||
{
|
||||
source: '1603716844054',
|
||||
target: '1603716858435',
|
||||
outputPortId: '1603716844054_out_1',
|
||||
inputPortId: '1603716858435_in_2',
|
||||
},
|
||||
{
|
||||
source: '1603716854368',
|
||||
target: '1603716868041',
|
||||
outputPortId: '1603716854368_out_1',
|
||||
inputPortId: '1603716868041_in_1',
|
||||
},
|
||||
],
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
import get from 'lodash/get'
|
||||
import set from 'lodash/set'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
|
||||
let state = {
|
||||
idx: 0,
|
||||
running: false,
|
||||
statusRes: {
|
||||
lang: 'zh_CN',
|
||||
success: true,
|
||||
data: {
|
||||
instStatus: {
|
||||
'10571193': 'success',
|
||||
'10571194': 'success',
|
||||
'10571195': 'success',
|
||||
'10571196': 'success',
|
||||
'10571197': 'success',
|
||||
},
|
||||
execInfo: {
|
||||
'10571193': {
|
||||
jobStatus: 'success',
|
||||
defName: '读数据表',
|
||||
name: 'germany_credit_data',
|
||||
id: 10571193,
|
||||
},
|
||||
'10571194': {
|
||||
jobStatus: 'success',
|
||||
defName: '离散值特征分析',
|
||||
name: '离散值特征分析',
|
||||
id: 10571194,
|
||||
},
|
||||
'10571195': {
|
||||
jobStatus: 'success',
|
||||
defName: '分箱',
|
||||
startTime: '2020-10-19 13:28:55',
|
||||
endTime: '2020-10-19 13:30:20',
|
||||
name: '分箱',
|
||||
id: 10571195,
|
||||
},
|
||||
'10571196': {
|
||||
jobStatus: 'success',
|
||||
defName: '评分卡训练',
|
||||
startTime: '2020-10-19 13:28:55',
|
||||
endTime: '2020-10-19 13:32:02',
|
||||
name: '评分卡训练-1',
|
||||
id: 10571196,
|
||||
},
|
||||
},
|
||||
status: 'default',
|
||||
},
|
||||
Lang: 'zh_CN',
|
||||
} as any,
|
||||
}
|
||||
|
||||
export const runGraph = async (nodes: any[]) => {
|
||||
const newState = getStatus()
|
||||
newState.data.instStatus = {}
|
||||
newState.data.execInfo = {}
|
||||
nodes.forEach((node) => {
|
||||
newState.data.instStatus[node.id] = 'default'
|
||||
newState.data.execInfo[node.id] = {
|
||||
jobStatus: 'default',
|
||||
defName: node.name,
|
||||
startTime: '2020-10-19 13:28:55',
|
||||
endTime: '2020-10-19 13:32:02',
|
||||
name: node.name,
|
||||
id: 10571196,
|
||||
}
|
||||
})
|
||||
state.running = true
|
||||
state.idx = 0
|
||||
state.statusRes = newState
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export const stopGraphRun = () => {
|
||||
state.running = false
|
||||
state.idx = 0
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const getStatus = () => cloneDeep(state.statusRes)
|
||||
|
||||
export const queryGraphStatus = async () => {
|
||||
const newState = getStatus()
|
||||
// console.log('Call Api QueryGraphStatus', state)
|
||||
if (state.running) {
|
||||
const { instStatus, execInfo } = newState.data
|
||||
const idList = Object.keys(instStatus)
|
||||
if (state.idx === idList.length) {
|
||||
state.idx = 0
|
||||
state.running = false
|
||||
idList.forEach((id) => {
|
||||
set(instStatus, id, 'success')
|
||||
set(execInfo, `${id}.jobStatus`, 'success')
|
||||
set(newState, 'data.status', 'success')
|
||||
})
|
||||
return newState
|
||||
}
|
||||
const key = get(idList, state.idx)
|
||||
set(instStatus, key, 'running')
|
||||
set(execInfo, `${key}.jobStatus`, 'running')
|
||||
set(newState, 'data.status', 'running')
|
||||
state.idx += 1
|
||||
return newState
|
||||
}
|
||||
return newState
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { useCallback, useState } from 'react'
|
||||
import { algoData, searchByKeyword } from '../mock/algo'
|
||||
|
||||
export namespace Res {
|
||||
export interface Data {
|
||||
defs: NodeDef[]
|
||||
cats: Cat[]
|
||||
}
|
||||
|
||||
export interface NodeDef {
|
||||
up: number
|
||||
down: number
|
||||
defSource: number
|
||||
catName: string
|
||||
isDeprecated: boolean
|
||||
isSubscribed: boolean
|
||||
isEnabled: boolean
|
||||
iconType: number
|
||||
docUrl: string
|
||||
sequence: number
|
||||
author?: string
|
||||
ioType: number
|
||||
lastModifyTime: string
|
||||
createdTime: string
|
||||
catId: number
|
||||
isComposite: boolean
|
||||
codeName: string
|
||||
engineType?: number
|
||||
description?: string
|
||||
name: string
|
||||
id: number
|
||||
type: number
|
||||
owner: string
|
||||
algoSourceType?: number
|
||||
}
|
||||
|
||||
export interface Cat {
|
||||
defSource: number
|
||||
isEnabled: boolean
|
||||
iconType: number
|
||||
codeName: string
|
||||
description: string
|
||||
sequence: number
|
||||
name: string
|
||||
id: number
|
||||
category?: string
|
||||
}
|
||||
}
|
||||
|
||||
function dfs(
|
||||
path = '',
|
||||
nodes: any[],
|
||||
isTarget: (node: any) => boolean,
|
||||
result: string[] = [],
|
||||
) {
|
||||
nodes.forEach((node, idx) => {
|
||||
if (node.children) {
|
||||
const currentIdx = path ? `${path}.${idx}.children` : `${idx}.children`
|
||||
dfs(currentIdx, node.children, isTarget, result)
|
||||
}
|
||||
|
||||
if (isTarget(node)) {
|
||||
const currentIdx = path ? `${path}.${idx}` : idx
|
||||
result.push(currentIdx.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [keyword, setKeyword] = useState<string>('') // 搜索关键字
|
||||
const [loading, setLoading] = useState<boolean>(false) // 加载状态
|
||||
const [componentTreeNodes, setComponentTreeNodes] = useState<any[]>([])
|
||||
const [searchList, setSearchList] = useState<any[]>([]) // 搜索结果列表
|
||||
|
||||
// 加载组件
|
||||
const loadComponentNodes = useCallback(() => {
|
||||
setLoading(true)
|
||||
const load = async () => {
|
||||
try {
|
||||
if (algoData) {
|
||||
setComponentTreeNodes(algoData)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return load()
|
||||
}, [])
|
||||
|
||||
// 搜索组件
|
||||
const search = useCallback((params: { keyword: string }) => {
|
||||
setKeyword(params.keyword ? params.keyword : '')
|
||||
if (!params.keyword) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const nodes = ([] = await searchByKeyword(params.keyword))
|
||||
setSearchList(nodes)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
// 状态
|
||||
keyword,
|
||||
loading,
|
||||
componentTreeNodes,
|
||||
searchList,
|
||||
|
||||
// 方法
|
||||
setKeyword,
|
||||
loadComponentNodes,
|
||||
search,
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
@import (reference) '~antd/es/style/themes/default.less';
|
||||
|
||||
.handler {
|
||||
position: absolute;
|
||||
top: 61px;
|
||||
right: 14px;
|
||||
z-index: 99;
|
||||
width: 32px;
|
||||
margin: 0;
|
||||
padding: 3px 0;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 16px;
|
||||
list-style-type: none;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.01);
|
||||
|
||||
.item {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #000;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover {
|
||||
:global {
|
||||
.@{ant-prefix}-popover-inner-content {
|
||||
padding: 3px 8px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Popover } from 'antd'
|
||||
import {
|
||||
CompressOutlined,
|
||||
OneToOneOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import classNames from 'classnames'
|
||||
import styles from './index.less'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
onZoomIn: () => void
|
||||
onZoomOut: () => void
|
||||
onFitContent: () => void
|
||||
onRealContent: () => void
|
||||
}
|
||||
|
||||
export const CanvasHandler: React.FC<Props> = (props) => {
|
||||
const { className, onZoomIn, onZoomOut, onFitContent, onRealContent } = props
|
||||
|
||||
return (
|
||||
<ul className={classNames(styles.handler, className)}>
|
||||
<Popover
|
||||
overlayClassName={styles.popover}
|
||||
content="放大"
|
||||
placement="left"
|
||||
>
|
||||
<li onClick={onZoomIn} className={styles.item}>
|
||||
<ZoomInOutlined />
|
||||
</li>
|
||||
</Popover>
|
||||
<Popover
|
||||
overlayClassName={styles.popover}
|
||||
content="缩小"
|
||||
placement="left"
|
||||
>
|
||||
<li onClick={onZoomOut} className={styles.item}>
|
||||
<ZoomOutOutlined />
|
||||
</li>
|
||||
</Popover>
|
||||
<Popover
|
||||
overlayClassName={styles.popover}
|
||||
content="实际尺寸"
|
||||
placement="left"
|
||||
>
|
||||
<li onClick={onRealContent} className={styles.item}>
|
||||
<OneToOneOutlined />
|
||||
</li>
|
||||
</Popover>
|
||||
<Popover
|
||||
overlayClassName={styles.popover}
|
||||
content="适应画布"
|
||||
placement="left"
|
||||
>
|
||||
<li onClick={onFitContent} className={styles.item}>
|
||||
<CompressOutlined />
|
||||
</li>
|
||||
</Popover>
|
||||
</ul>
|
||||
)
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { Graph } from '@antv/x6'
|
||||
|
||||
Graph.registerConnector(
|
||||
'pai',
|
||||
(s, t) => {
|
||||
const offset = 4
|
||||
const control = 80
|
||||
const v1 = { x: s.x, y: s.y + offset + control }
|
||||
const v2 = { x: t.x, y: t.y - offset - control }
|
||||
|
||||
return `M ${s.x} ${s.y}
|
||||
L ${s.x} ${s.y + offset}
|
||||
C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${t.x} ${t.y - offset}
|
||||
L ${t.x} ${t.y}
|
||||
`
|
||||
},
|
||||
true,
|
||||
)
|
@ -1,39 +0,0 @@
|
||||
.list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 220px;
|
||||
.item {
|
||||
font-size: 12px;
|
||||
padding: 0 0 0;
|
||||
line-height: 16px;
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
width: 220px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1 0 45px;
|
||||
text-align: right;
|
||||
padding-right: 4px;
|
||||
position: relative;
|
||||
word-break: break-all;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
&after {
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
.text {
|
||||
padding-left: 4px;
|
||||
flex: 3 0 100px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
:global(.aicontent-popover-inner-content) {
|
||||
padding: 12px 8px 8px 8px;
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Popover } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import css from './index.less'
|
||||
|
||||
interface TooltipProps {
|
||||
children: React.ReactElement
|
||||
status: StatusObj
|
||||
}
|
||||
|
||||
interface StatusObj {
|
||||
name: string
|
||||
defName: string
|
||||
jobStatus: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
}
|
||||
|
||||
export const NodePopover = ({ children, status }: TooltipProps) => {
|
||||
const componentNode = (
|
||||
<div style={{ width: '100%', height: '100%' }}>{children}</div>
|
||||
)
|
||||
if (isEmpty(status)) {
|
||||
return componentNode
|
||||
}
|
||||
return (
|
||||
<Popover
|
||||
placement="bottomLeft"
|
||||
content={<PopoverContent status={status} />}
|
||||
overlayClassName={css.content}
|
||||
>
|
||||
{componentNode}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const nodeAtts: StatusObj = {
|
||||
name: '节点名称',
|
||||
defName: '算法名称',
|
||||
jobStatus: '运行状态',
|
||||
startTime: '开始时间',
|
||||
endTime: '结束时间',
|
||||
}
|
||||
|
||||
const PopoverContent = ({ status }: { status: StatusObj }) => (
|
||||
<ul className={css.list}>
|
||||
{!status.name && <LoadingOutlined />}
|
||||
{Object.entries(nodeAtts).map(([key, text]) => {
|
||||
const value = status[key as keyof StatusObj]
|
||||
if (value) {
|
||||
return (
|
||||
<li key={key} className={css.item}>
|
||||
<span className={css.label}>{text}</span>
|
||||
<span className={css.text}>{value}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</ul>
|
||||
)
|
@ -1,40 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
status: 'success' | 'fail' | 'running' | 'ready' | 'upChangeSuccess'
|
||||
}
|
||||
|
||||
export const NodeStatus: React.FC<Props> = (props) => {
|
||||
const { className, status } = props
|
||||
switch (status) {
|
||||
case 'fail':
|
||||
return (
|
||||
<div className={className}>
|
||||
<CloseCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
</div>
|
||||
)
|
||||
case 'success':
|
||||
case 'upChangeSuccess': {
|
||||
const color = status === 'success' ? '#2ecc71' : '#1890ff'
|
||||
return (
|
||||
<div className={className}>
|
||||
<CheckCircleOutlined style={{ color }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
case 'running':
|
||||
return (
|
||||
<div className={className}>
|
||||
<SyncOutlined spin={true} style={{ color: '#1890ff' }} />
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
.x6-edge {
|
||||
&-selected,
|
||||
&:hover {
|
||||
path[stroke-width='1'] {
|
||||
stroke-width: 3px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import { Shape, Edge } from '@antv/x6'
|
||||
import './edge.less'
|
||||
|
||||
export class BaseEdge extends Shape.Edge {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
isGroupEdge() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export class GuideEdge extends BaseEdge {}
|
||||
|
||||
GuideEdge.config({
|
||||
shape: 'GuideEdge',
|
||||
connector: { name: 'pai' },
|
||||
zIndex: 2,
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#808080',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
stroke: 'none',
|
||||
fill: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export class X6DemoGroupEdge extends GuideEdge {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
isGroupEdge() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
X6DemoGroupEdge.config({
|
||||
shape: 'X6DemoGroupEdge',
|
||||
})
|
||||
|
||||
Edge.registry.register({
|
||||
GuideEdge: GuideEdge as any,
|
||||
X6DemoGroupEdge: X6DemoGroupEdge as any,
|
||||
})
|
@ -1,150 +0,0 @@
|
||||
import { Dom, Node } from '@antv/x6'
|
||||
import { ReactShape } from '@antv/x6-react-shape'
|
||||
import { NODE_WIDTH, NODE_HEIGHT } from '@/constants/graph'
|
||||
|
||||
export class BaseNode extends ReactShape {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
isGroup() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export class X6DemoNode extends BaseNode {
|
||||
getInPorts() {
|
||||
return this.getPortsByGroup('in')
|
||||
}
|
||||
|
||||
getOutPorts() {
|
||||
return this.getPortsByGroup('out')
|
||||
}
|
||||
}
|
||||
|
||||
Node.registry.register('ais-rect-port', X6DemoNode as any)
|
||||
|
||||
X6DemoNode.config({
|
||||
width: NODE_WIDTH,
|
||||
height: NODE_HEIGHT,
|
||||
shape: 'ais-rect-port',
|
||||
ports: {
|
||||
groups: {
|
||||
in: {
|
||||
position: { name: 'top' },
|
||||
zIndex: 2,
|
||||
},
|
||||
out: {
|
||||
position: { name: 'bottom' },
|
||||
zIndex: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
attrs: {
|
||||
body: {
|
||||
magnet: false,
|
||||
fill: 'none',
|
||||
stroke: 'none',
|
||||
refWidth: '100%',
|
||||
refHeight: '100%',
|
||||
zIndex: 1,
|
||||
},
|
||||
},
|
||||
portMarkup: [
|
||||
{
|
||||
tagName: 'foreignObject',
|
||||
selector: 'fo',
|
||||
attrs: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
x: -3,
|
||||
y: -3,
|
||||
zIndex: 10,
|
||||
// magnet决定是否可交互
|
||||
magnet: 'true',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
ns: Dom.ns.xhtml,
|
||||
tagName: 'body',
|
||||
selector: 'foBody',
|
||||
attrs: {
|
||||
xmlns: Dom.ns.xhtml,
|
||||
},
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
tagName: 'span',
|
||||
selector: 'content',
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export class X6DemoGroupNode extends BaseNode {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
isGroup() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
X6DemoGroupNode.config({
|
||||
ports: {
|
||||
groups: {
|
||||
in: {
|
||||
position: { name: 'top' },
|
||||
zIndex: 2,
|
||||
},
|
||||
out: {
|
||||
position: { name: 'bottom' },
|
||||
zIndex: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
portMarkup: [
|
||||
{
|
||||
tagName: 'foreignObject',
|
||||
selector: 'fo',
|
||||
attrs: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
x: -3,
|
||||
y: -3,
|
||||
zIndex: 10,
|
||||
// magnet决定是否可交互
|
||||
magnet: 'true',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
ns: Dom.ns.xhtml,
|
||||
tagName: 'body',
|
||||
selector: 'foBody',
|
||||
attrs: {
|
||||
xmlns: Dom.ns.xhtml,
|
||||
},
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
tagName: 'span',
|
||||
selector: 'content',
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
@ -1,14 +0,0 @@
|
||||
import { useEffect, MutableRefObject } from 'react'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
export const useSafeSetHTML = (
|
||||
ref: MutableRefObject<Element | null>,
|
||||
htmlStr: string = '',
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (ref?.current instanceof Element && typeof htmlStr === 'string') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
ref.current.innerHTML = DOMPurify.sanitize(htmlStr)
|
||||
}
|
||||
}, [htmlStr])
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
.x6-toolbar-overwrite {
|
||||
:global {
|
||||
.x6-toolbar {
|
||||
height: 36px !important;
|
||||
overflow: visible;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.x6-toolbar-content {
|
||||
overflow: visible;
|
||||
|
||||
.x6-toolbar-group::before {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.x6-toolbar-item {
|
||||
margin: 6px 0 !important;
|
||||
padding: 0 12px !important;
|
||||
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.x6-edge {
|
||||
stroke-width: 1px;
|
||||
&.success {
|
||||
path:nth-child(2) {
|
||||
stroke: #888 !important;
|
||||
}
|
||||
path:nth-child(3) {
|
||||
fill: #888 !important;
|
||||
stroke: #888 !important;
|
||||
}
|
||||
}
|
||||
&.error {
|
||||
stroke-width: 2px;
|
||||
path:nth-child(2) {
|
||||
stroke: rgba(245, 34, 45, 0.45) !important;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
&.edgeProcessing {
|
||||
path:nth-child(2) {
|
||||
stroke: rgba(57, 202, 116, 0.8);
|
||||
stroke-width: 2px;
|
||||
stroke-dasharray: 8px, 2px;
|
||||
&:local {
|
||||
animation: processing-line 30s infinite linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes processing-line {
|
||||
to {
|
||||
stroke-dashoffset: -1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
.x6-split-box-horizontal > .x6-split-box-resizer,
|
||||
.x6-split-box-vertical > .x6-split-box-resizer {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-spin-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.x6-widget-selection-inner {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.x6-widget-selection-box {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
import { maxBy, minBy } from 'lodash-es'
|
||||
import { NExperimentGraph } from '@/pages/rx-models/typing'
|
||||
|
||||
interface BasicPoint {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 找出一组坐标的边缘坐标(最小和最大的边缘坐标轴)和中点
|
||||
* @param points
|
||||
*/
|
||||
export function calcPointsInfo(points: BasicPoint[]) {
|
||||
if (!Array.isArray(points) || !points.length) {
|
||||
throw new Error('计算坐标边缘必须传入一组坐标')
|
||||
}
|
||||
const minX = minBy(points, (point: BasicPoint) => point.x)!.x
|
||||
const minY = minBy(points, (point: BasicPoint) => point.y)!.y
|
||||
const maxX = maxBy(points, (point: BasicPoint) => point.x)!.x
|
||||
const maxY = maxBy(points, (point: BasicPoint) => point.y)!.y
|
||||
const middleX = (minX + maxX) / 2
|
||||
const middleY = (minY + maxY) / 2
|
||||
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
middleX,
|
||||
middleY,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一组坐标转换成相对某个点的相对租表
|
||||
* @param points
|
||||
* @param origin
|
||||
*/
|
||||
export function transformPointsToOrigin(
|
||||
points: BasicPoint[],
|
||||
origin: BasicPoint,
|
||||
): BasicPoint[] {
|
||||
return points.map((point) => ({
|
||||
...point,
|
||||
x: point.x - origin.x,
|
||||
y: point.y - origin.y,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一组相对某点的坐标还原回原始坐标
|
||||
* @param points
|
||||
* @param origin
|
||||
*/
|
||||
export function revertPointsToOrigin(
|
||||
points: BasicPoint[],
|
||||
origin: BasicPoint,
|
||||
): BasicPoint[] {
|
||||
return points.map((point) => ({
|
||||
...point,
|
||||
x: point.x + origin.x,
|
||||
y: point.y + origin.y,
|
||||
}))
|
||||
}
|
||||
|
||||
export function formatNodeToGraphNodeConf(originNode: {
|
||||
id: number
|
||||
nodeInstanceId?: number
|
||||
positionX: number
|
||||
positionY: number
|
||||
}): any {
|
||||
const { id, nodeInstanceId, positionX, positionY } = originNode
|
||||
return {
|
||||
...originNode,
|
||||
x: positionX || 0,
|
||||
y: positionY || 0,
|
||||
id: (nodeInstanceId || id)!.toString(),
|
||||
width: 180,
|
||||
height: 32,
|
||||
data: originNode,
|
||||
ports: {
|
||||
groups: {
|
||||
inputPorts: {
|
||||
position: {
|
||||
name: 'top',
|
||||
args: {
|
||||
dr: 0,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
},
|
||||
},
|
||||
attrs: {
|
||||
circle: {
|
||||
fill: '#ffffff',
|
||||
stroke: '#31d0c6',
|
||||
strokeWidth: 1,
|
||||
r: 4,
|
||||
style: 'cursor: default;',
|
||||
},
|
||||
text: {
|
||||
fill: '#6a6c8a',
|
||||
},
|
||||
},
|
||||
},
|
||||
outputPorts: {
|
||||
position: {
|
||||
name: 'bottom',
|
||||
args: {
|
||||
dr: 0,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
},
|
||||
},
|
||||
attrs: {
|
||||
circle: {
|
||||
fill: '#ffffff',
|
||||
stroke: '#31d0c6',
|
||||
strokeWidth: 1,
|
||||
r: 4,
|
||||
style: 'cursor: crosshair;',
|
||||
},
|
||||
text: {
|
||||
fill: '#6a6c8a',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 将实验图节点信息转换为节点和边的配置
|
||||
* @param graph
|
||||
*/
|
||||
export function formatExperimentGraph(graph: any = {}) {
|
||||
const { nodes = [], links = [], groups = [] } = graph
|
||||
const formattedNodes = nodes.map((node: any) =>
|
||||
formatNodeToGraphNodeConf(node),
|
||||
)
|
||||
|
||||
const formattedEdges = links.map((link: any) => {
|
||||
const { source, target } = link
|
||||
return {
|
||||
...link,
|
||||
source: source.toString(),
|
||||
target: target.toString(),
|
||||
label: '',
|
||||
}
|
||||
})
|
||||
|
||||
const groupNodeMap = groups.reduce(
|
||||
(mapResult: any, currentGroup: NExperimentGraph.Group) => {
|
||||
const { id } = currentGroup
|
||||
return {
|
||||
...mapResult,
|
||||
[id]:
|
||||
formattedNodes.filter(
|
||||
(node: any) => node.groupId.toString() === id.toString(),
|
||||
) || [],
|
||||
}
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
return {
|
||||
nodes: formattedNodes,
|
||||
edges: formattedEdges,
|
||||
groups,
|
||||
groupNodeMap,
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { Graph } from '@antv/x6'
|
||||
|
||||
// 将画布上的点转换成相对于 offset parent 的点
|
||||
export function graphPointToOffsetPoint(
|
||||
graph: Graph,
|
||||
graphPoint: { x: number; y: number },
|
||||
containerElem: HTMLElement,
|
||||
) {
|
||||
if (graph) {
|
||||
const point = graph!.localToPage({ x: graphPoint.x, y: graphPoint.y })
|
||||
const clientRect = containerElem?.getBoundingClientRect()
|
||||
const y = point.y - (clientRect?.y || 0) // ! offset parent 不能是画布容器,否则会影响内部布局,所以 offset parent 在外部,算上上方 toolbar 的高度
|
||||
const x = point.x - (clientRect?.x || 0)
|
||||
return { x, y }
|
||||
}
|
||||
return { x: 0, y: 0 }
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Form, Input, Radio } from 'antd'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
|
||||
export interface Props {
|
||||
name: string
|
||||
experimentId: string
|
||||
}
|
||||
|
||||
export const ExperimentForm: React.FC<Props> = ({ experimentId, name }) => {
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
const [activeExperiment] = useObservableState(expGraph.experiment$)
|
||||
|
||||
const onValuesChange = ({ experimentName }: { experimentName: string }) => {
|
||||
expGraph.experiment$.next({ ...activeExperiment, name: experimentName })
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
experimentName: activeExperiment ? activeExperiment.name : '',
|
||||
})
|
||||
}, [activeExperiment])
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
experimentName: activeExperiment ? activeExperiment.name : '',
|
||||
}}
|
||||
onValuesChange={onValuesChange}
|
||||
requiredMark={false}
|
||||
>
|
||||
<Form.Item name="experimentName" label="实验名称">
|
||||
<Input placeholder="input placeholder" />
|
||||
</Form.Item>
|
||||
<Form.Item label={name}>
|
||||
<Input placeholder="input placeholder" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Field C">
|
||||
<Input placeholder="input placeholder" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Field D">
|
||||
<Input placeholder="input placeholder" />
|
||||
</Form.Item>
|
||||
<Form.Item label="RadioDemo">
|
||||
<Radio.Group>
|
||||
<Radio.Button value="optional">Optional</Radio.Button>
|
||||
<Radio.Button value={true}>Required</Radio.Button>
|
||||
<Radio.Button value={false}>Hidden</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Form, Input } from 'antd'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import 'antd/lib/style/index.css'
|
||||
|
||||
export interface Props {
|
||||
name: string
|
||||
experimentId: string
|
||||
nodeId: string
|
||||
}
|
||||
|
||||
export const NodeFormDemo: React.FC<Props> = ({
|
||||
name,
|
||||
nodeId,
|
||||
experimentId,
|
||||
}) => {
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
const [node] = useObservableState(() => expGraph.activeNodeInstance$)
|
||||
|
||||
const onValuesChange = async ({ name }: { name: string }) => {
|
||||
if (node.name !== name) {
|
||||
await expGraph.renameNode(nodeId, name)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{ name: node ? node.name : '' }}
|
||||
onValuesChange={onValuesChange}
|
||||
requiredMark={false}
|
||||
>
|
||||
<Form.Item label="节点名称" name="name">
|
||||
<Input placeholder="input placeholder" />
|
||||
</Form.Item>
|
||||
<Form.Item label={name}>
|
||||
<Input placeholder="input placeholder" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Field C">
|
||||
<Input placeholder="input placeholder" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Field D">
|
||||
<Input placeholder="input placeholder" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
@border-color: #e9e9e9;
|
||||
|
||||
.setting {
|
||||
height: calc(~'100% - 41px');
|
||||
:global {
|
||||
.ant-row.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ant-tabs-nav-list {
|
||||
width: 100%;
|
||||
}
|
||||
.ant-tabs-card .ant-tabs-content {
|
||||
margin-top: -16px;
|
||||
}
|
||||
.ant-tabs-card .ant-tabs-content > .ant-tabs-tabpane {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
.ant-tabs-card > .ant-tabs-nav::before {
|
||||
display: none;
|
||||
}
|
||||
.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab,
|
||||
.ant-tabs-tab {
|
||||
flex: 1 0 50px;
|
||||
padding: 7px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.ant-tabs-tab.ant-tabs-tab-active {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.ant-tabs-card .ant-tabs-tab,
|
||||
[data-theme='compact'] .ant-tabs-card .ant-tabs-tab {
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
.ant-tabs-card .ant-tabs-tab-active,
|
||||
[data-theme='compact'] .ant-tabs-card .ant-tabs-tab-active {
|
||||
border-color: #fff;
|
||||
background: #fff;
|
||||
}
|
||||
#components-tabs-demo-card-top .code-box-demo {
|
||||
background: #f5f5f5;
|
||||
overflow: hidden;
|
||||
padding: 24px;
|
||||
}
|
||||
[data-theme='compact'] .ant-tabs-card .ant-tabs-content {
|
||||
margin-top: -8px;
|
||||
}
|
||||
[data-theme='dark'] .ant-tabs-card .ant-tabs-tab {
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
[data-theme='dark'] #components-tabs-demo-card-top .code-box-demo {
|
||||
background: #000;
|
||||
}
|
||||
[data-theme='dark'] .ant-tabs-card .ant-tabs-content > .ant-tabs-tabpane {
|
||||
background: #141414;
|
||||
}
|
||||
[data-theme='dark'] .ant-tabs-card .ant-tabs-tab-active {
|
||||
border-color: #141414;
|
||||
background: #141414;
|
||||
}
|
||||
.ant-tabs-content {
|
||||
height: calc(~'100vh - 125px');
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
height: calc(100vh - 100px);
|
||||
flex-grow: 1;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: relative;
|
||||
flex-grow: 0;
|
||||
height: 41px;
|
||||
min-height: 41px;
|
||||
padding: 8px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 -2px 6px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Tabs } from 'antd'
|
||||
import classNames from 'classnames'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { ExperimentForm } from './form/experiment-config'
|
||||
import { NodeFormDemo } from './form/node-config'
|
||||
import css from './index.less'
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const ComponentConfigPanel: React.FC<Props> = (props) => {
|
||||
const { experimentId, className } = props
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
const [activeNodeInstance] = useObservableState(
|
||||
() => expGraph.activeNodeInstance$,
|
||||
)
|
||||
|
||||
const nodeId = activeNodeInstance && activeNodeInstance.id
|
||||
|
||||
return (
|
||||
<div className={classNames(className, css.confPanel)}>
|
||||
<div className={css.setting}>
|
||||
<Tabs
|
||||
defaultActiveKey="setting"
|
||||
type="card"
|
||||
size="middle"
|
||||
tabPosition="top"
|
||||
destroyInactiveTabPane={true}
|
||||
>
|
||||
<Tabs.TabPane tab="参数设置" key="setting">
|
||||
<div className={css.form}>
|
||||
{nodeId && (
|
||||
<NodeFormDemo
|
||||
name="节点参数"
|
||||
nodeId={nodeId}
|
||||
experimentId={experimentId}
|
||||
/>
|
||||
)}
|
||||
{!nodeId && (
|
||||
<ExperimentForm name="实验设置" experimentId={experimentId} />
|
||||
)}
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="全局参数" key="params" disabled={true}>
|
||||
<div className={css.form} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className={css.footer} />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
@import (reference) '~antd/es/style/themes/default.less';
|
||||
|
||||
.list {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
|
||||
.tree {
|
||||
height: 100%;
|
||||
background-image: linear-gradient(#fff 50%, #f7f9fb 50%);
|
||||
background-size: 100% 60px;
|
||||
|
||||
.treeFolder:global(.@{ant-prefix}-tree-treenode) {
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
|
||||
&::before {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.@{ant-prefix}-tree-iconEle.@{ant-prefix}-tree-icon__customize {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.@{ant-prefix}-tree-node-content-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 0;
|
||||
|
||||
.@{ant-prefix}-tree-title {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.treeNode:global(.@{ant-prefix}-tree-treenode) {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
&::before {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.@{ant-prefix}-tree-node-content-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-tree-iconEle.@{ant-prefix}-tree-icon__customize {
|
||||
display: none;
|
||||
}
|
||||
.@{ant-prefix}-tree-switcher {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { useModel } from 'umi'
|
||||
import { Tree } from 'antd'
|
||||
import { FolderFilled, FolderOpenFilled } from '@ant-design/icons'
|
||||
import { NodeTitle } from './node-title'
|
||||
import styles from './index.less'
|
||||
|
||||
const { DirectoryTree, TreeNode } = Tree
|
||||
|
||||
const FolderIcon = ({ expanded }: { expanded: boolean }) => {
|
||||
return expanded ? <FolderOpenFilled /> : <FolderFilled />
|
||||
}
|
||||
|
||||
export const CategoryTree = () => {
|
||||
const { componentTreeNodes } = useModel('guide-algo-component')
|
||||
|
||||
const renderTree = useCallback(
|
||||
(treeList: any[] = [], searchKey: string = '') => {
|
||||
return treeList.map((item) => {
|
||||
const { isDir, id, children } = item
|
||||
const key = id.toString()
|
||||
const title = <NodeTitle node={item} searchKey={searchKey} />
|
||||
|
||||
if (isDir) {
|
||||
return (
|
||||
<TreeNode
|
||||
icon={FolderIcon}
|
||||
key={key}
|
||||
title={title}
|
||||
className={styles.treeFolder}
|
||||
>
|
||||
{renderTree(children, searchKey)}
|
||||
</TreeNode>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeNode
|
||||
isLeaf={true}
|
||||
key={key}
|
||||
icon={<span />}
|
||||
title={title}
|
||||
className={styles.treeNode}
|
||||
/>
|
||||
)
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const treeList = componentTreeNodes.filter((node) => node.status !== 4)
|
||||
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
<DirectoryTree
|
||||
showIcon={true}
|
||||
selectable={false}
|
||||
autoExpandParent={true}
|
||||
className={styles.tree}
|
||||
defaultExpandedKeys={['recentlyUsed']}
|
||||
>
|
||||
{renderTree(treeList)}
|
||||
</DirectoryTree>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
@import (reference) '~antd/es/style/themes/default.less';
|
||||
|
||||
.nodeTitleWrapper {
|
||||
&:hover {
|
||||
.node {
|
||||
width: 180px;
|
||||
margin-left: 20px;
|
||||
padding-left: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.08);
|
||||
|
||||
.nodeIcon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
margin-right: 0;
|
||||
color: #1890ff;
|
||||
background-color: rgba(229, 238, 255, 0.85);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-left: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
padding-left: 20px;
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
|
||||
.nodeIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 4px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.keyword {
|
||||
color: #f50;
|
||||
}
|
||||
|
||||
.folder {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: 8px;
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
width: 184px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.team {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.@{ant-prefix}-tree-title {
|
||||
:local {
|
||||
.doc {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
color: transparent; // 防止拖拽时文档两个字被带进去
|
||||
font-size: 10px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover {
|
||||
width: 360px;
|
||||
overflow: hidden;
|
||||
.monitor {
|
||||
margin: 8px 0;
|
||||
b {
|
||||
display: block;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
.doclink {
|
||||
padding: 8px 0 0;
|
||||
}
|
||||
.description {
|
||||
max-height: 600px;
|
||||
padding: 8px 4px;
|
||||
overflow: auto;
|
||||
word-break: break-all;
|
||||
background: #fbfbfb;
|
||||
border-radius: 4px;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
a,
|
||||
img,
|
||||
p,
|
||||
pre {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
font-size: 12px !important;
|
||||
line-height: 1.5em;
|
||||
word-break: break-all !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin: 4px 0;
|
||||
.label {
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
import React, { useCallback, useState, useRef } from 'react'
|
||||
import { toLower, unescape } from 'lodash-es'
|
||||
import { Popover, Tag } from 'antd'
|
||||
import { DragSource, ConnectDragPreview, ConnectDragSource } from 'react-dnd'
|
||||
import { DatabaseFilled, ReadOutlined } from '@ant-design/icons'
|
||||
import marked from 'marked'
|
||||
import { useSafeSetHTML } from '@/pages/common/hooks/useSafeSetHtml'
|
||||
import { DRAGGABLE_ALGO_COMPONENT } from '@/constants/graph'
|
||||
import styles from './node-title.less'
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
})
|
||||
|
||||
const Document = (props: { node: any }) => {
|
||||
const { node } = props
|
||||
const descriptionNodeRef = useRef<HTMLDivElement>(null)
|
||||
const { description, id, tag = '' } = node
|
||||
|
||||
const htmlStr = marked(
|
||||
unescape(description || '暂无文档').replace(/\\n/gi, ' \n '),
|
||||
)
|
||||
useSafeSetHTML(descriptionNodeRef, htmlStr)
|
||||
|
||||
return (
|
||||
<div className={styles.popover}>
|
||||
{tag ? (
|
||||
<div className={styles.tag}>
|
||||
<span className={styles.label}> 标签: </span>
|
||||
{tag.split(',').map((str: string) => (
|
||||
<Tag key={str}>{str}</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.description}>
|
||||
<div ref={descriptionNodeRef} />
|
||||
<div className={styles.doclink}>
|
||||
<a href={`#/${id}`} target="_blank" rel="noopener noreferrer">
|
||||
查看更多
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
node: any
|
||||
searchKey: string
|
||||
isDragging: boolean
|
||||
connectDragSource: ConnectDragSource
|
||||
connectDragPreview: ConnectDragPreview
|
||||
}
|
||||
|
||||
const InnerNodeTitle = (props: Props) => {
|
||||
const {
|
||||
node = {},
|
||||
searchKey = '',
|
||||
connectDragPreview,
|
||||
connectDragSource,
|
||||
} = props
|
||||
const { name = '', isDir } = node
|
||||
const [visible, setVisible] = useState<boolean>(false)
|
||||
const onMouseIn = useCallback(() => {
|
||||
setVisible(true)
|
||||
}, [])
|
||||
const onMouseOut = useCallback(() => {
|
||||
setVisible(false)
|
||||
}, [])
|
||||
|
||||
// 文件夹
|
||||
if (isDir) {
|
||||
return <div className={styles.folder}>{name}</div>
|
||||
}
|
||||
|
||||
const keywordIdx = searchKey ? toLower(name).indexOf(toLower(searchKey)) : -1
|
||||
|
||||
// 搜索高亮
|
||||
if (keywordIdx > -1) {
|
||||
const beforeStr = name.substr(0, keywordIdx)
|
||||
const afterStr = name.substr(keywordIdx + searchKey.length)
|
||||
|
||||
return connectDragPreview(
|
||||
connectDragSource(
|
||||
<span className={styles.node}>
|
||||
<DatabaseFilled className={styles.nodeIcon} />
|
||||
<span className={styles.label}>
|
||||
{beforeStr}
|
||||
<span className={styles.keyword}>{searchKey}</span>
|
||||
{afterStr}
|
||||
</span>
|
||||
</span>,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.nodeTitleWrapper}
|
||||
onMouseEnter={onMouseIn}
|
||||
onMouseLeave={onMouseOut}
|
||||
>
|
||||
{connectDragPreview(
|
||||
connectDragSource(
|
||||
<div className={styles.node}>
|
||||
<DatabaseFilled className={styles.nodeIcon} />
|
||||
<span className={styles.label}>{name}</span>
|
||||
</div>,
|
||||
),
|
||||
)}
|
||||
{visible && (
|
||||
<Popover
|
||||
visible={true}
|
||||
title={name}
|
||||
placement="right"
|
||||
content={<Document node={node} />}
|
||||
key="description"
|
||||
>
|
||||
<a className={styles.doc}>
|
||||
<ReadOutlined /> 文档
|
||||
</a>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const NodeTitle = DragSource(
|
||||
DRAGGABLE_ALGO_COMPONENT,
|
||||
{
|
||||
beginDrag: (props: Props) => ({
|
||||
component: props.node,
|
||||
}),
|
||||
},
|
||||
(connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
connectDragPreview: connect.dragPreview(),
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
)(InnerNodeTitle)
|
@ -1,4 +0,0 @@
|
||||
.componentTree {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import { useModel } from 'umi'
|
||||
import { useMount } from 'ahooks'
|
||||
import { CategoryTree } from './category-tree'
|
||||
import { SearchResultList } from './search-result-list'
|
||||
import styles from './index.less'
|
||||
|
||||
export const ComponentTree = () => {
|
||||
const { keyword, loadComponentNodes } = useModel('guide-algo-component')
|
||||
|
||||
useMount(() => {
|
||||
loadComponentNodes()
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.componentTree}>
|
||||
{keyword ? <SearchResultList /> : <CategoryTree />}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
@itemHeight: 30px;
|
||||
|
||||
.nodeItem {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.itemBlock {
|
||||
position: relative;
|
||||
|
||||
.nodeItem {
|
||||
max-width: 150px;
|
||||
height: @itemHeight;
|
||||
padding: 0 5px 0 2.2px;
|
||||
line-height: @itemHeight;
|
||||
cursor: move;
|
||||
|
||||
&:hover {
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.08);
|
||||
|
||||
.nodeIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: @itemHeight;
|
||||
min-width: @itemHeight;
|
||||
height: @itemHeight;
|
||||
color: #1890ff;
|
||||
background-color: rgba(229, 238, 255, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.nodeIcon {
|
||||
margin-right: 8px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.itemLable {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 158px;
|
||||
font-size: 10px;
|
||||
transform: scale(0.83);
|
||||
|
||||
&.gre {
|
||||
color: #2ecc71;
|
||||
}
|
||||
}
|
||||
|
||||
.catLable {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 158px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
transform: scale(0.83);
|
||||
}
|
||||
|
||||
.description {
|
||||
height: @itemHeight;
|
||||
padding-left: 22px;
|
||||
overflow: hidden;
|
||||
line-height: @itemHeight;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.link {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.link {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.componentDescription {
|
||||
max-width: 360px;
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
word-break: break-all;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
a,
|
||||
img,
|
||||
p {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
font-size: 12px !important;
|
||||
line-height: 1.5em;
|
||||
word-break: break-all !important;
|
||||
}
|
||||
}
|
||||
|
||||
.statusTips {
|
||||
max-width: 320px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin: 8px 0 4px;
|
||||
.label {
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
@ -1,184 +0,0 @@
|
||||
import React, { useRef } from 'react'
|
||||
import classname from 'classnames'
|
||||
import { Popover, Tag, Tooltip } from 'antd'
|
||||
import { DatabaseFilled, ProfileTwoTone } from '@ant-design/icons'
|
||||
import marked from 'marked'
|
||||
import { ConnectDragPreview, ConnectDragSource, DragSource } from 'react-dnd'
|
||||
import { ItemName } from '@/component/item-name'
|
||||
import { unescape } from '@/common/utils'
|
||||
import { useSafeSetHTML } from '@/pages/common/hooks/useSafeSetHtml'
|
||||
import { DRAGGABLE_ALGO_COMPONENT } from '@/constants/graph'
|
||||
import styles from './component-item.less'
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
})
|
||||
|
||||
const Markdown2html: React.FC<{ description: string; tag: string }> = (
|
||||
props,
|
||||
) => {
|
||||
const { description, tag } = props
|
||||
const descriptionElementRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useSafeSetHTML(
|
||||
descriptionElementRef,
|
||||
marked(unescape(description).replace(/\\n/gi, ' \n ')),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.componentDescription}>
|
||||
<div ref={descriptionElementRef} key="1" />
|
||||
{tag ? (
|
||||
<div className={styles.tag} key="2">
|
||||
<span className={styles.label}> 标签: </span>
|
||||
{tag.split(',').map((str, idx) => (
|
||||
<Tag key={str + idx}>{str}</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSearchInfo = (params: {
|
||||
id: number | string
|
||||
name: string
|
||||
catName: string
|
||||
description: string
|
||||
tag: string
|
||||
}) => {
|
||||
const { id, name, catName, description = '暂无数据', tag } = params
|
||||
|
||||
return (
|
||||
<>
|
||||
{catName && (
|
||||
<span className={`${styles.catLable} gray `} key="catName">
|
||||
{catName}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className={styles.link} key="link">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://pai.alipay.com/component/detail/${id}`}
|
||||
>
|
||||
<Tooltip title="查看文档">
|
||||
<ProfileTwoTone />
|
||||
</Tooltip>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
{description && (
|
||||
<Popover
|
||||
title={name}
|
||||
placement="right"
|
||||
content={<Markdown2html description={description} tag={tag} />}
|
||||
key="description"
|
||||
>
|
||||
<div
|
||||
className={classname(styles.description, 'gray', 'text-overflow')}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ! 这里没有理解怎么会走到渲染这个链路上,因此代码先保留,后续可以再删掉或者使用
|
||||
const renderStatus = (params: {
|
||||
changeType: string
|
||||
isDeprecated: boolean
|
||||
changeMessage: string
|
||||
}) => {
|
||||
const { changeType, isDeprecated, changeMessage } = params
|
||||
const renderItems = []
|
||||
if (changeType) {
|
||||
renderItems.push(
|
||||
<Popover
|
||||
content={<p className={styles.statusTips}>{changeMessage}</p>}
|
||||
key="changeType"
|
||||
>
|
||||
<span className={classname(styles.itemLable, styles.gre)}>
|
||||
{changeType.toLowerCase()}
|
||||
</span>
|
||||
</Popover>,
|
||||
)
|
||||
}
|
||||
|
||||
if (isDeprecated) {
|
||||
renderItems.push(
|
||||
<span className={classname(styles.itemLable, 'gray')} key="status">
|
||||
已废弃
|
||||
</span>,
|
||||
)
|
||||
}
|
||||
|
||||
return renderItems
|
||||
}
|
||||
|
||||
interface Node {
|
||||
keyword: string
|
||||
algoSourceType: number
|
||||
name: string
|
||||
id: number
|
||||
catName: string
|
||||
description: string
|
||||
tag: string
|
||||
changeType: string
|
||||
isDeprecated: boolean
|
||||
changeMessage: string
|
||||
}
|
||||
|
||||
interface NodeTitleProps {
|
||||
data: Node
|
||||
connectDragSource: ConnectDragSource
|
||||
connectDragPreview: ConnectDragPreview
|
||||
}
|
||||
|
||||
const InnerNodeTitle: React.FC<NodeTitleProps> = (props) => {
|
||||
const { data, connectDragPreview, connectDragSource } = props
|
||||
const { keyword, algoSourceType, name } = data
|
||||
return (
|
||||
<div>
|
||||
{connectDragPreview(
|
||||
connectDragSource(
|
||||
<span
|
||||
className={classname(styles.nodeItem, {
|
||||
[styles.orange]: algoSourceType === 2,
|
||||
})}
|
||||
>
|
||||
<DatabaseFilled className={styles.nodeIcon} />
|
||||
<ItemName data={{ name, keyword }} />
|
||||
</span>,
|
||||
),
|
||||
)}
|
||||
{keyword ? renderSearchInfo(data) : renderStatus(data)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NodeTitle = DragSource(
|
||||
DRAGGABLE_ALGO_COMPONENT,
|
||||
{
|
||||
beginDrag: (props: NodeTitleProps) => ({
|
||||
component: props.data,
|
||||
}),
|
||||
},
|
||||
(connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
connectDragPreview: connect.dragPreview(),
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
)(InnerNodeTitle)
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
|
||||
export const ComponentItem: React.FC<Props> = ({ data = {} }) => {
|
||||
return <div className={styles.itemBlock}>{<NodeTitle data={data} />}</div>
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
.searchList {
|
||||
min-height: calc(~'100vh - 80px');
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
background-image: linear-gradient(#fff 50%, #f7f9fb 50%);
|
||||
background-size: 100% 60px;
|
||||
|
||||
:global {
|
||||
li {
|
||||
padding: 0 4px 0 10px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: #e3f4ff;
|
||||
}
|
||||
}
|
||||
|
||||
.flag {
|
||||
padding-right: 4px;
|
||||
color: #1890ff;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resultTips {
|
||||
height: 24px;
|
||||
margin-top: 16px;
|
||||
color: #a0a0a0;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { useModel } from 'umi'
|
||||
import { Spin } from 'antd'
|
||||
import { ProfileTwoTone } from '@ant-design/icons'
|
||||
|
||||
import { ComponentItem } from './component-item'
|
||||
import styles from './index.less'
|
||||
|
||||
export const SearchResultList = () => {
|
||||
const { keyword, searchList, loading } = useModel('guide-algo-component')
|
||||
|
||||
const renderList = useCallback((list: any[], keywd: string) => {
|
||||
return (
|
||||
<ul className={styles.searchList}>
|
||||
{list.map((component, idx) => (
|
||||
<li key={idx.toString()}>
|
||||
<ComponentItem data={{ ...component, keyword: keywd }} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}, [])
|
||||
|
||||
const renderEmptyResult = useCallback(() => {
|
||||
return (
|
||||
<>
|
||||
<p className={styles.resultTips}>
|
||||
<ProfileTwoTone />
|
||||
{'没有搜索结果'}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<div className="tree-wrapper">
|
||||
{searchList.length
|
||||
? renderList(searchList, keyword)
|
||||
: renderEmptyResult()}
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
@import (reference) '~antd/es/style/themes/default.less';
|
||||
|
||||
.componentSourceTree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
background-color: #f7f9fb;
|
||||
|
||||
.component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.links {
|
||||
flex-grow: 0;
|
||||
height: 41px;
|
||||
min-height: 41px;
|
||||
padding: 8px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 -2px 6px 0 rgba(0, 0, 0, 0.08);
|
||||
|
||||
.link:global(.@{ant-prefix}-btn) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
height: 20px;
|
||||
padding-bottom: 0;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:last-of-type {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
:global {
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { SearchInput } from './search-input'
|
||||
import { ComponentTree } from './component-tree'
|
||||
import styles from './index.less'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const ComponentSourceTree: React.FC<Props> = (props) => {
|
||||
const { className } = props
|
||||
return (
|
||||
<div className={classNames(className, styles.componentSourceTree)}>
|
||||
<div className={styles.component}>
|
||||
<SearchInput />
|
||||
<ComponentTree />
|
||||
</div>
|
||||
<div className={styles.links} />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
@import (reference) '~antd/es/style/themes/default.less';
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
|
||||
.input {
|
||||
:global(.@{ant-prefix}-input) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Input } from 'antd'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { useModel } from 'umi'
|
||||
import styles from './index.less'
|
||||
|
||||
const { Search } = Input
|
||||
|
||||
export const SearchInput = () => {
|
||||
const [value, setValue] = useState<string>('')
|
||||
const { search, setKeyword } = useModel('guide-algo-component')
|
||||
|
||||
const { run: onDebouncedSearch } = useDebounceFn(
|
||||
(v: string) => {
|
||||
if (!v) {
|
||||
return
|
||||
}
|
||||
search({ keyword: v })
|
||||
},
|
||||
{ wait: 500 },
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.searchInput}>
|
||||
<Search
|
||||
className={styles.input}
|
||||
placeholder="请输入组件名称或描述"
|
||||
value={value}
|
||||
allowClear={true}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
if (!v) {
|
||||
setKeyword('')
|
||||
}
|
||||
setValue(v)
|
||||
}}
|
||||
onSearch={onDebouncedSearch}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
.nodeSourceTreeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.tabWrapper {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 12px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-of-type {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #1890ff;
|
||||
background-color: #f7f9fb;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabContentWrapper {
|
||||
flex-grow: 1;
|
||||
max-height: calc(100% - 33px);
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { ComponentSourceTree } from './component-source-tree'
|
||||
import styles from './index.less'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
experimentId: string
|
||||
}
|
||||
|
||||
type TabOption = 'component' | 'model'
|
||||
|
||||
export const ComponentTreePanel: React.FC<Props> = (props) => {
|
||||
const { className } = props
|
||||
const [activeTab, setActiveTab] = useState<TabOption>('component')
|
||||
|
||||
return (
|
||||
<div className={classNames(className, styles.nodeSourceTreeContainer)}>
|
||||
<div className={styles.tabWrapper}>
|
||||
<div
|
||||
className={classNames(styles.tab, {
|
||||
[styles.active]: activeTab === 'component',
|
||||
})}
|
||||
onClick={() => {
|
||||
setActiveTab('component')
|
||||
}}
|
||||
>
|
||||
组件库
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.tabContentWrapper}>
|
||||
<ComponentSourceTree
|
||||
className={classNames({ [styles.hide]: activeTab !== 'component' })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
@import (reference) '~antd/es/style/themes/default.less';
|
||||
|
||||
.bottomToolbar {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 199;
|
||||
height: 40px;
|
||||
font-size: 12px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 16px -5px rgba(0, 0, 0, 0.2);
|
||||
user-select: none;
|
||||
|
||||
.itemList {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
text-align: center;
|
||||
list-style-type: none;
|
||||
|
||||
.item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
margin: 0 10px;
|
||||
padding: 0;
|
||||
line-height: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
|
||||
a {
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
|
||||
a {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
& .anticon {
|
||||
margin-right: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
padding: 4px 0;
|
||||
|
||||
:global {
|
||||
.@{ant-prefix}-menu-item {
|
||||
min-width: 110px;
|
||||
height: 32px;
|
||||
margin: 0 !important;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
line-height: 32px;
|
||||
|
||||
a::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-selected {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
:local {
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menuPopover {
|
||||
:global {
|
||||
.@{ant-prefix}-popover-inner {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
&-content {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover {
|
||||
:global {
|
||||
.@{ant-prefix}-popover-inner-content {
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Popover } from 'antd'
|
||||
import {
|
||||
CloudUploadOutlined,
|
||||
LogoutOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import classNames from 'classnames'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import styles from './bottom-toolbar.less'
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
}
|
||||
|
||||
export const BottomToolbar: React.FC<Props> = (props) => {
|
||||
const { experimentId } = props
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
const [running] = useObservableState(expGraph.running$)
|
||||
const [preparingRun, setPreparingRun] = useState(false)
|
||||
const [preparingStop, setPreparingStop] = useState(false)
|
||||
|
||||
// running 的值发生变化,说明运行或停止按钮的操作产生了作用
|
||||
useEffect(() => {
|
||||
setPreparingRun(false)
|
||||
setPreparingStop(false)
|
||||
}, [running])
|
||||
|
||||
// 运行实验
|
||||
const onRunExperiment = useCallback(() => {
|
||||
setPreparingRun(true)
|
||||
expGraph.runGraph().then((res: any) => {
|
||||
if (!res.success) {
|
||||
setPreparingRun(false)
|
||||
}
|
||||
})
|
||||
}, [expGraph])
|
||||
|
||||
// 停止运行
|
||||
const onStopRunExperiment = useCallback(() => {
|
||||
setPreparingStop(true)
|
||||
expGraph.stopRunGraph().then((res: any) => {
|
||||
if (!res.success) {
|
||||
setPreparingStop(false)
|
||||
}
|
||||
})
|
||||
}, [expGraph])
|
||||
|
||||
const runningConfigs = [
|
||||
{
|
||||
content: '运行',
|
||||
tip: '依次运行本实验的每个组件',
|
||||
icon: PlayCircleOutlined,
|
||||
disabled: preparingRun,
|
||||
clickHandler: onRunExperiment,
|
||||
},
|
||||
{
|
||||
content: '停止',
|
||||
tip: '停止运行实验',
|
||||
icon: LogoutOutlined,
|
||||
disabled: preparingStop,
|
||||
clickHandler: onStopRunExperiment,
|
||||
},
|
||||
]
|
||||
|
||||
const runningConfig = runningConfigs[Number(!!running)]
|
||||
const RunningIcon = runningConfig.icon
|
||||
|
||||
return (
|
||||
<div className={styles.bottomToolbar}>
|
||||
<ul className={styles.itemList}>
|
||||
{/* 部署 */}
|
||||
<li className={styles.item}>
|
||||
<CloudUploadOutlined />
|
||||
<span>部署</span>
|
||||
</li>
|
||||
|
||||
{/* 运行/停止 */}
|
||||
<Popover content={runningConfig.tip} overlayClassName={styles.popover}>
|
||||
<li
|
||||
className={classNames(styles.item, {
|
||||
[styles.disabled]: runningConfig.disabled,
|
||||
})}
|
||||
onClick={runningConfig.clickHandler}
|
||||
>
|
||||
<RunningIcon />
|
||||
<span>{runningConfig.content}</span>
|
||||
</li>
|
||||
</Popover>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
.canvasContent {
|
||||
overflow: hidden;
|
||||
background-color: #f7f7fa;
|
||||
position: relative;
|
||||
|
||||
.runningStatus {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 48px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
import { message } from 'antd'
|
||||
import '@antv/x6-react-shape'
|
||||
import { useDrop } from 'react-dnd'
|
||||
import classNames from 'classnames'
|
||||
import { DRAGGABLE_ALGO_COMPONENT, DRAGGABLE_MODEL } from '@/constants/graph'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { FloatingContextMenu } from './elements/floating-context-menu'
|
||||
import { CanvasHandler } from '../common/canvas-handler'
|
||||
import { GraphRunningStatus } from './elements/graph-running-status'
|
||||
import styles from './canvas-content.less'
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CanvasContent: React.FC<Props> = (props) => {
|
||||
const { experimentId, className } = props
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const canvasRef = useRef<HTMLDivElement | null>(null)
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
|
||||
// 渲染画布
|
||||
useEffect(() => {
|
||||
expGraph.renderGraph(containerRef.current!, canvasRef.current!)
|
||||
}, [expGraph])
|
||||
|
||||
// 处理组件拖拽落下事件
|
||||
const [, dropRef] = useDrop({
|
||||
accept: [DRAGGABLE_ALGO_COMPONENT, DRAGGABLE_MODEL],
|
||||
drop: (item: any, monitor) => {
|
||||
const currentMouseOffset = monitor.getClientOffset()
|
||||
const sourceMouseOffset = monitor.getInitialClientOffset()
|
||||
const sourceElementOffset = monitor.getInitialSourceClientOffset()
|
||||
const diffX = sourceMouseOffset!.x - sourceElementOffset!.x
|
||||
const diffY = sourceMouseOffset!.y - sourceElementOffset!.y
|
||||
const x = currentMouseOffset!.x - diffX
|
||||
const y = currentMouseOffset!.y - diffY
|
||||
if (expGraph.isGraphReady()) {
|
||||
expGraph.requestAddNode({
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
nodeMeta: item.component,
|
||||
})
|
||||
} else {
|
||||
message.info('实验数据建立中,请稍后再尝试添加节点')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// 画布侧边 toolbar handler
|
||||
const onHandleSideToolbar = useCallback(
|
||||
(action: 'in' | 'out' | 'fit' | 'real') => () => {
|
||||
// 确保画布已渲染
|
||||
if (expGraph.isGraphReady()) {
|
||||
switch (action) {
|
||||
case 'in':
|
||||
expGraph.zoomGraph(0.1)
|
||||
break
|
||||
case 'out':
|
||||
expGraph.zoomGraph(-0.1)
|
||||
break
|
||||
case 'fit':
|
||||
expGraph.zoomGraphToFit()
|
||||
break
|
||||
case 'real':
|
||||
expGraph.zoomGraphRealSize()
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
},
|
||||
[expGraph],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(elem) => {
|
||||
containerRef.current = elem
|
||||
dropRef(elem)
|
||||
}}
|
||||
className={classNames(className, styles.canvasContent)}
|
||||
>
|
||||
{/* 图和边的右键菜单 */}
|
||||
<FloatingContextMenu experimentId={experimentId} />
|
||||
|
||||
{/* 缩放相关的 toolbar */}
|
||||
<CanvasHandler
|
||||
onZoomIn={onHandleSideToolbar('in')}
|
||||
onZoomOut={onHandleSideToolbar('out')}
|
||||
onFitContent={onHandleSideToolbar('fit')}
|
||||
onRealContent={onHandleSideToolbar('real')}
|
||||
/>
|
||||
|
||||
{/* 图运行状态 */}
|
||||
<GraphRunningStatus
|
||||
className={styles.runningStatus}
|
||||
experimentId={experimentId}
|
||||
/>
|
||||
|
||||
{/* 图容器 */}
|
||||
<div ref={canvasRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
@import (reference) '../common/style/x6-overwrite';
|
||||
|
||||
.canvasToolbar {
|
||||
background-color: #f7f9fb;
|
||||
|
||||
.x6-toolbar-overwrite();
|
||||
|
||||
.searchItem {
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.searchWrapper {
|
||||
position: relative;
|
||||
|
||||
.search {
|
||||
width: 200px;
|
||||
background-color: transparent;
|
||||
animation: expansion 0.2s ease-in-out;
|
||||
|
||||
@keyframes expansion {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.@{ant-prefix}-input {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchResult {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 200px;
|
||||
max-height: 200px;
|
||||
padding: 4px 0;
|
||||
overflow-y: auto;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
list-style-type: none;
|
||||
background-color: #fefefe;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.searchResultItem {
|
||||
padding: 0 8px;
|
||||
line-height: 24px;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(243, 249, 255, 0.92);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,168 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Toolbar } from '@antv/x6-react-components'
|
||||
import {
|
||||
GatewayOutlined,
|
||||
GroupOutlined,
|
||||
PlaySquareOutlined,
|
||||
RollbackOutlined,
|
||||
UngroupOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { RxInput } from '@/component/rx-component/rx-input'
|
||||
import { showModal } from '@/component/modal'
|
||||
import { addNodeGroup } from '@/mock/graph'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { formatGroupInfoToNodeMeta } from '@/pages/rx-models/graph-util'
|
||||
import styles from './canvas-toolbar.less'
|
||||
|
||||
const { Item, Group } = Toolbar
|
||||
interface Props {
|
||||
experimentId: string
|
||||
}
|
||||
|
||||
enum Operations {
|
||||
UNDO_DELETE = 'UNDO_DELETE',
|
||||
GROUP_SELECT = 'GROUP_SELECT',
|
||||
RUN_SELECTED = 'RUN_SELECTED',
|
||||
NEW_GROUP = 'NEW_GROUP',
|
||||
UNGROUP = 'UNGROUP',
|
||||
}
|
||||
|
||||
export const CanvasToolbar: React.FC<Props> = (props) => {
|
||||
const { experimentId } = props
|
||||
const [selectionEnabled, setSelectionEnabled] = useState<boolean>(false)
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
const [activeNodeInstance] = useObservableState(
|
||||
() => expGraph.activeNodeInstance$,
|
||||
)
|
||||
const [selectedNodes] = useObservableState(() => expGraph.selectedNodes$)
|
||||
const [selectedGroup] = useObservableState(() => expGraph.selectedGroup$)
|
||||
|
||||
const onClickItem = useCallback(
|
||||
(itemName: string) => {
|
||||
switch (itemName) {
|
||||
case Operations.UNDO_DELETE:
|
||||
expGraph.undoDeleteNode()
|
||||
break
|
||||
case Operations.GROUP_SELECT:
|
||||
expGraph.toggleSelectionEnabled()
|
||||
setSelectionEnabled((enabled) => !enabled)
|
||||
break
|
||||
case Operations.RUN_SELECTED:
|
||||
expGraph.runGraph()
|
||||
break
|
||||
case Operations.NEW_GROUP: {
|
||||
const value$ = new BehaviorSubject('')
|
||||
const modal = showModal({
|
||||
title: '新建分组',
|
||||
width: 450,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
children: (
|
||||
<div
|
||||
style={{ fontSize: 12, display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<div style={{ width: 50, marginBottom: 8 }}>组名:</div>
|
||||
<RxInput
|
||||
value={value$}
|
||||
onChange={(e) => {
|
||||
value$.next(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
onOk: () => {
|
||||
modal.update({ okButtonProps: { loading: true } })
|
||||
addNodeGroup(value$.getValue())
|
||||
.then((res: any) => {
|
||||
modal.close()
|
||||
selectedNodes!.forEach((node) => {
|
||||
const nodeData = node.getData<any>()
|
||||
node.setData({ ...nodeData, groupId: res.data.group.id })
|
||||
})
|
||||
const nodeMetas: any[] = selectedNodes!.map((node) =>
|
||||
node.getData<any>(),
|
||||
)
|
||||
expGraph.addNode(
|
||||
formatGroupInfoToNodeMeta(res.data.group, nodeMetas),
|
||||
)
|
||||
expGraph.unSelectNode()
|
||||
})
|
||||
.finally(() => {
|
||||
modal.update({ okButtonProps: { loading: false } })
|
||||
})
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
case Operations.UNGROUP: {
|
||||
const descendantNodes = selectedGroup!.getDescendants()
|
||||
const childNodes = descendantNodes.filter((node) => node.isNode())
|
||||
childNodes.forEach((node) => {
|
||||
const nodeData = node.getData<any>()
|
||||
node.setData({ ...nodeData, groupId: 0 })
|
||||
})
|
||||
selectedGroup!.setChildren([])
|
||||
expGraph.deleteNodes(selectedGroup!)
|
||||
expGraph.unSelectNode()
|
||||
break
|
||||
}
|
||||
default:
|
||||
}
|
||||
},
|
||||
[expGraph, activeNodeInstance, selectedNodes, experimentId, selectedGroup],
|
||||
)
|
||||
|
||||
const newGroupEnabled =
|
||||
!!selectedNodes &&
|
||||
!!selectedNodes.length &&
|
||||
selectedNodes.length > 1 &&
|
||||
selectedNodes.every((node) => {
|
||||
return node.isNode() && !node.getData<any>().groupId
|
||||
})
|
||||
|
||||
const unGroupEnabled = !selectedNodes?.length && !!selectedGroup
|
||||
|
||||
return (
|
||||
<div className={styles.canvasToolbar}>
|
||||
<Toolbar hoverEffect={true} onClick={onClickItem}>
|
||||
<Group>
|
||||
<Item
|
||||
name={Operations.UNDO_DELETE}
|
||||
tooltip="撤销删除节点"
|
||||
icon={<RollbackOutlined />}
|
||||
/>
|
||||
<Item
|
||||
name={Operations.GROUP_SELECT}
|
||||
active={selectionEnabled}
|
||||
tooltip="框选节点"
|
||||
icon={<GatewayOutlined />}
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Item
|
||||
name={Operations.NEW_GROUP}
|
||||
disabled={!newGroupEnabled}
|
||||
tooltip="新建群组"
|
||||
icon={<GroupOutlined />}
|
||||
/>
|
||||
<Item
|
||||
name={Operations.UNGROUP}
|
||||
disabled={!unGroupEnabled}
|
||||
tooltip="拆分群组"
|
||||
icon={<UngroupOutlined />}
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Item
|
||||
name={Operations.RUN_SELECTED}
|
||||
disabled={!activeNodeInstance}
|
||||
tooltip="执行选择节点"
|
||||
icon={<PlaySquareOutlined />}
|
||||
/>
|
||||
</Group>
|
||||
</Toolbar>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
.edgeContextMenu {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background-color: #fff;
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { Menu } from '@antv/x6-react-components'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { graphPointToOffsetPoint } from '@/pages/common//utils/graph'
|
||||
import styles from './index.less'
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
data: any
|
||||
}
|
||||
|
||||
export const EdgeContextMenu: React.FC<Props> = (props) => {
|
||||
const { experimentId, data } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null as any)
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
|
||||
useClickAway(() => {
|
||||
expGraph.clearContextMenuInfo()
|
||||
}, containerRef)
|
||||
|
||||
const onDeleteEdge = useCallback(() => {
|
||||
expGraph.deleteEdgeFromContextMenu(data.edge)
|
||||
}, [expGraph, data])
|
||||
|
||||
const { x: left, y: top } = graphPointToOffsetPoint(
|
||||
expGraph.graph!,
|
||||
data,
|
||||
expGraph.wrapper!,
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={styles.edgeContextMenu}
|
||||
style={{ top, left }}
|
||||
>
|
||||
<Menu hasIcon={true}>
|
||||
<Menu.Item
|
||||
onClick={onDeleteEdge}
|
||||
icon={<DeleteOutlined />}
|
||||
text="删除"
|
||||
/>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.graphContextMenu {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.loadExperimentModal {
|
||||
:global {
|
||||
.@{ant-prefix}-modal-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.versionTable {
|
||||
:global {
|
||||
.@{ant-prefix}-table.@{ant-prefix}-table-small {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.operations {
|
||||
:global {
|
||||
.@{ant-prefix}-btn {
|
||||
&:first-of-type {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
&.@{ant-prefix}-btn-sm {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import React, { useRef } from 'react'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { Menu } from '@antv/x6-react-components'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { graphPointToOffsetPoint } from '@/pages/common//utils/graph'
|
||||
import styles from './index.less'
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
data: any
|
||||
}
|
||||
|
||||
export const GraphContextMenu: React.FC<Props> = (props) => {
|
||||
const { experimentId, data } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null as any)
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
|
||||
useClickAway(() => {
|
||||
expGraph.clearContextMenuInfo()
|
||||
}, containerRef)
|
||||
|
||||
const { x: left, y: top } = graphPointToOffsetPoint(
|
||||
expGraph.graph!,
|
||||
data,
|
||||
expGraph.wrapper!,
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={styles.graphContextMenu}
|
||||
style={{ top, left }}
|
||||
>
|
||||
<Menu hasIcon={true}>
|
||||
<Menu.Item disabled={true} icon={<ReloadOutlined />} text="刷新" />
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.graphContextMenu {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.loadExperimentModal {
|
||||
:global {
|
||||
.@{ant-prefix}-modal-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.versionTable {
|
||||
:global {
|
||||
.@{ant-prefix}-table.@{ant-prefix}-table-small {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.operations {
|
||||
:global {
|
||||
.@{ant-prefix}-btn {
|
||||
&:first-of-type {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
&.@{ant-prefix}-btn-sm {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import {
|
||||
PlaySquareOutlined,
|
||||
EditOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { Menu } from '@antv/x6-react-components'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { graphPointToOffsetPoint } from '@/pages/common//utils/graph'
|
||||
import styles from './index.less'
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
data: any
|
||||
}
|
||||
|
||||
export const NodeContextMenu: React.FC<Props> = (props) => {
|
||||
const { experimentId, data } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null as any)
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
const [activeNodeInstance] = useObservableState(
|
||||
() => expGraph.activeNodeInstance$,
|
||||
)
|
||||
|
||||
useClickAway(() => {
|
||||
expGraph.clearContextMenuInfo()
|
||||
}, containerRef)
|
||||
|
||||
const onNodeCopy = useCallback(async () => {
|
||||
await expGraph.onCopyNode(data.node)
|
||||
}, [expGraph, activeNodeInstance])
|
||||
|
||||
const onNodeDel = useCallback(async () => {
|
||||
await expGraph.requestDeleteNodes([data.node.id])
|
||||
}, [expGraph, activeNodeInstance])
|
||||
|
||||
const onGraphRun = useCallback(async () => {
|
||||
await expGraph.runGraph()
|
||||
}, [expGraph])
|
||||
|
||||
const { x: left, y: top } = graphPointToOffsetPoint(
|
||||
expGraph.graph!,
|
||||
data,
|
||||
expGraph.wrapper!,
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={styles.graphContextMenu}
|
||||
style={{ top, left }}
|
||||
>
|
||||
<Menu hasIcon={true}>
|
||||
<Menu.Item onClick={onNodeCopy} icon={<CopyOutlined />} text="复制" />
|
||||
<Menu.Item onClick={onNodeDel} icon={<DeleteOutlined />} text="删除" />
|
||||
<Menu.Item disabled={true} icon={<EditOutlined />} text="重命名" />
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
onClick={onGraphRun}
|
||||
icon={<PlaySquareOutlined />}
|
||||
text="执行"
|
||||
/>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 99999;
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import React from 'react'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { EdgeContextMenu } from './context-menu/edge-context-menu'
|
||||
import { GraphContextMenu } from './context-menu/graph-context-menu'
|
||||
import { NodeContextMenu } from './context-menu/node-context-menu'
|
||||
import css from './floating-context-menu.less'
|
||||
|
||||
interface ContextMenuProps {
|
||||
experimentId: string
|
||||
menuType: 'node' | 'edge' | 'graph'
|
||||
menuData: any
|
||||
}
|
||||
|
||||
export const ContextMenu: React.FC<ContextMenuProps> = (props) => {
|
||||
const { experimentId, menuType, menuData } = props
|
||||
|
||||
switch (menuType) {
|
||||
case 'edge':
|
||||
return <EdgeContextMenu experimentId={experimentId} data={menuData} />
|
||||
case 'graph':
|
||||
return <GraphContextMenu experimentId={experimentId} data={menuData} />
|
||||
case 'node':
|
||||
return <NodeContextMenu experimentId={experimentId} data={menuData} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
}
|
||||
|
||||
export const FloatingContextMenu: React.FC<Props> = (props) => {
|
||||
const { experimentId } = props
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
const [contextMenuInfo] = useObservableState(() => expGraph.contextMenuInfo$)
|
||||
|
||||
if (!contextMenuInfo?.type) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css.mask}>
|
||||
<ContextMenu
|
||||
experimentId={experimentId}
|
||||
menuData={contextMenuInfo.data}
|
||||
menuType={contextMenuInfo.type}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import React from 'react'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
experimentId: string
|
||||
}
|
||||
|
||||
export const GraphRunningStatus: React.FC<Props> = (props) => {
|
||||
const { className, experimentId } = props
|
||||
const experimentGraph = useExperimentGraph(experimentId)
|
||||
const [executionStatus] = useObservableState(
|
||||
() => experimentGraph.executionStatus$,
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{executionStatus?.status === 'preparing' && (
|
||||
<>
|
||||
<LoadingOutlined style={{ marginRight: 4 }} /> 准备中...
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
.nodeElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 180px;
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.08);
|
||||
|
||||
&.selected,
|
||||
&:hover {
|
||||
background-color: rgba(243, 249, 255, 0.92);
|
||||
border: 1px solid #1890ff;
|
||||
box-shadow: 0 0 3px 3px rgba(64, 169, 255, 0.2);
|
||||
|
||||
.icon {
|
||||
width: 32px;
|
||||
height: 30px;
|
||||
margin: 0 1px 0 -1px;
|
||||
}
|
||||
|
||||
// .notation {
|
||||
// position: relative;
|
||||
// left: -1px;
|
||||
|
||||
// :global {
|
||||
// .anticon {
|
||||
// position: relative;
|
||||
// right: -2px;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
flex-grow: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: rgba(229, 238, 255, 0.85);
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.notation {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 32px);
|
||||
padding: 0 8px;
|
||||
user-select: none;
|
||||
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow-x: hidden;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
display: inline-flex;
|
||||
flex-grow: 0;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.x6-node [magnet='true'] {
|
||||
cursor: crosshair;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.x6-node [magnet='true']:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.x6-node [magnet='true'][port-group='in'] {
|
||||
cursor: move;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.x6-port-body > span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ais-port {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #808080;
|
||||
border-radius: 100%;
|
||||
background: #fff;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ais-port.connected {
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-top: 3px;
|
||||
margin-left: -1px;
|
||||
border-width: 5px 4px 0;
|
||||
border-style: solid;
|
||||
border-color: #808080 transparent transparent;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.x6-port-body.available {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.x6-port-body.available body {
|
||||
overflow: visible;
|
||||
}
|
||||
.x6-port-body.available span {
|
||||
overflow: visible;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.x6-port-body.available body > span::before {
|
||||
content: ' ';
|
||||
float: left;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -6px;
|
||||
margin-left: -6px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(57, 202, 116, 0.6);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.x6-port-body.available body > span::after {
|
||||
content: ' ';
|
||||
float: left;
|
||||
clear: both;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: -15px;
|
||||
margin-left: -1px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #39ca74;
|
||||
// position: relative;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
// transform: translateZ(0);
|
||||
// position: relative
|
||||
}
|
||||
|
||||
.x6-port-body.adsorbed {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.x6-port-body.adsorbed body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.x6-port-body.adsorbed body > span::before {
|
||||
content: ' ';
|
||||
float: left;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-top: -9px;
|
||||
margin-left: -10px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(57, 202, 116, 0.6);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.x6-port-body.adsorbed body > span::after {
|
||||
content: ' ';
|
||||
float: left;
|
||||
clear: both;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: -19px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #39ca74;
|
||||
// position: relative;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Node } from '@antv/x6'
|
||||
import classNames from 'classnames'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import { filter, map } from 'rxjs/operators'
|
||||
import { DatabaseFilled } from '@ant-design/icons'
|
||||
import { useObservableState } from '@/common/hooks/useObservableState'
|
||||
import { ANT_PREFIX } from '@/constants/global'
|
||||
import { NExecutionStatus } from '@/pages/rx-models/typing'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { NodeStatus } from '@/pages/common/graph-common/node-status'
|
||||
import { NodePopover } from '../../common/graph-common/node-popover'
|
||||
import styles from './node-element.less'
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
node?: Node
|
||||
}
|
||||
|
||||
export const NodeElement: React.FC<Props> = (props) => {
|
||||
const { experimentId, node } = props
|
||||
const experimentGraph = useExperimentGraph(experimentId)
|
||||
const [instanceStatus] = useObservableState(
|
||||
() =>
|
||||
experimentGraph.executionStatus$.pipe(
|
||||
filter((x) => !!x),
|
||||
map((x) => x.execInfo),
|
||||
),
|
||||
{} as NExecutionStatus.ExecutionStatus['execInfo'],
|
||||
)
|
||||
const data: any = node?.getData() || {}
|
||||
const { name, id, selected } = data
|
||||
const nodeStatus = instanceStatus[id] || {}
|
||||
|
||||
return (
|
||||
<ConfigProvider prefixCls={ANT_PREFIX}>
|
||||
<NodePopover status={nodeStatus}>
|
||||
<div
|
||||
className={classNames(styles.nodeElement, {
|
||||
[styles.selected]: !!selected,
|
||||
})}
|
||||
>
|
||||
<div className={styles.icon}>
|
||||
<DatabaseFilled style={{ color: '#1890ff' }} />
|
||||
</div>
|
||||
<div className={styles.notation}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
{nodeStatus.jobStatus && (
|
||||
<NodeStatus
|
||||
className={styles.statusIcon}
|
||||
status={nodeStatus.jobStatus as any}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NodePopover>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
@borderColor: #d9d9d9;
|
||||
|
||||
.nodeGroup {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
background-color: #fff;
|
||||
border: 1px dashed @borderColor;
|
||||
border-radius: 4px;
|
||||
cursor: default; // TODO: 暂时还不支持群组移动,先把鼠标变成不可拖动的类型
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
|
||||
.name {
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.collapseButton {
|
||||
z-index: 11;
|
||||
order: -1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
z-index: 11;
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.x6-node-selected {
|
||||
:local {
|
||||
.nodeGroup {
|
||||
background-color: rgba(243, 249, 255, 0.92);
|
||||
border: 1px solid #1890ff;
|
||||
box-shadow: 0 0 3px 3px rgba(64, 169, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.namePopover {
|
||||
:global {
|
||||
.@{ant-prefix}-popover-inner-content {
|
||||
padding: 8px 12px !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,367 +0,0 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { Popover, ConfigProvider } from 'antd'
|
||||
import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons'
|
||||
import { Edge } from '@antv/x6'
|
||||
import { ANT_PREFIX } from '@/constants/global'
|
||||
import { calcNodeScale } from '@/pages/rx-models/graph-util'
|
||||
import { useExperimentGraph } from '@/pages/rx-models/experiment-graph'
|
||||
import { NExperimentGraph } from '@/pages/rx-models/typing'
|
||||
import {
|
||||
X6DemoGroupNode,
|
||||
X6DemoNode,
|
||||
} from '@/pages/common/graph-common/shape/node'
|
||||
import { X6DemoGroupEdge } from '../../common/graph-common/shape/edge'
|
||||
import styles from './node-group.less'
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
node?: X6DemoGroupNode
|
||||
}
|
||||
|
||||
export const NodeGroup: React.FC<Props> = (props) => {
|
||||
const { experimentId, node } = props
|
||||
const data = node!.getData<NExperimentGraph.Group>()
|
||||
const { name, isCollapsed = false } = data
|
||||
const experimentGraph = useExperimentGraph(experimentId)
|
||||
|
||||
// 折叠
|
||||
const onCollapseGroup = useCallback(() => {
|
||||
const { graph } = experimentGraph
|
||||
const children = node!.getDescendants()
|
||||
const childNodes: X6DemoNode[] = children.filter((child) =>
|
||||
child.isNode(),
|
||||
) as any[]
|
||||
const { x, y, width, height } = calcNodeScale(
|
||||
{ isCollapsed: true } as any,
|
||||
childNodes.map((i) => i.getData()),
|
||||
)
|
||||
|
||||
// 还原尺寸
|
||||
node!.setProp({
|
||||
position: { x, y },
|
||||
size: { width, height },
|
||||
})
|
||||
|
||||
// 处理 Data
|
||||
node?.updateData({ isCollapsed: true })
|
||||
|
||||
// 隐藏子元素
|
||||
children.forEach((child) => {
|
||||
child.hide()
|
||||
child.updateData({ hide: true })
|
||||
})
|
||||
|
||||
// 删除桩和边
|
||||
const groupEdges = graph?.getConnectedEdges(node!)
|
||||
if (groupEdges?.length) {
|
||||
experimentGraph.deleteEdges(groupEdges)
|
||||
}
|
||||
const ports = node!.getPorts()
|
||||
if (ports?.length) {
|
||||
node!.removePorts(ports)
|
||||
}
|
||||
|
||||
// 创建新的连接桩和边
|
||||
const incomingEdges = childNodes.reduce(
|
||||
(accu: Edge[], curr: X6DemoNode) => {
|
||||
const incomingEdgs = graph!.getIncomingEdges(curr)
|
||||
if (incomingEdgs?.length) {
|
||||
return [...accu, ...incomingEdgs]
|
||||
}
|
||||
return accu
|
||||
},
|
||||
[] as Edge[],
|
||||
)
|
||||
const outgoingEdges = childNodes.reduce(
|
||||
(accu: Edge[], curr: X6DemoNode) => {
|
||||
const outgoingEdgs = graph!.getOutgoingEdges(curr)
|
||||
if (outgoingEdgs?.length) {
|
||||
return [...accu, ...outgoingEdgs]
|
||||
}
|
||||
return accu
|
||||
},
|
||||
[] as Edge[],
|
||||
)
|
||||
if (incomingEdges?.length) {
|
||||
const inputPortId = Date.now().toString()
|
||||
node!.addPort({ group: 'in', id: inputPortId, connected: true })
|
||||
incomingEdges
|
||||
.filter(
|
||||
(edge) =>
|
||||
!childNodes.map((i) => i.id).includes(edge.getSourceCellId()),
|
||||
) // 只考虑从外部连接到组里的连线
|
||||
.forEach((edge) => {
|
||||
let sourceNodeId = edge.getSourceCellId()
|
||||
let sourcePortId = edge.getSourcePortId()
|
||||
const sourceNode: X6DemoNode = edge.getSourceCell()! as X6DemoNode
|
||||
if (sourceNode instanceof X6DemoGroupNode) {
|
||||
experimentGraph.deleteEdges(edge)
|
||||
return
|
||||
}
|
||||
const sourceParentNode: X6DemoGroupNode | null =
|
||||
sourceNode.parent as X6DemoGroupNode
|
||||
// 若源节点不可见,且存在父群组节点,则说明源节点的父群组节点已折叠,此时创建一个连接折叠父节点的桩
|
||||
if (
|
||||
!sourceNode.visible &&
|
||||
sourceParentNode instanceof X6DemoGroupNode
|
||||
) {
|
||||
sourceNodeId = sourceParentNode.id
|
||||
const parentNodePorts = sourceParentNode.getPortsByGroup('out')
|
||||
sourcePortId = parentNodePorts[0].id
|
||||
}
|
||||
graph!.addEdge(
|
||||
new X6DemoGroupEdge({
|
||||
sourceAnchor: 'bottom',
|
||||
source: {
|
||||
cell: sourceNodeId,
|
||||
port: sourcePortId,
|
||||
anchor: {
|
||||
name: 'bottom',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
cell: node!.id,
|
||||
port: inputPortId,
|
||||
anchor: {
|
||||
name: 'center',
|
||||
},
|
||||
},
|
||||
zIndex: 1,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
if (outgoingEdges?.length) {
|
||||
const outputPortId = Date.now().toString()
|
||||
node!.addPort({ group: 'out', id: outputPortId, connected: false })
|
||||
outgoingEdges
|
||||
.filter(
|
||||
(edge) =>
|
||||
!childNodes.map((i) => i.id).includes(edge.getTargetCellId()),
|
||||
)
|
||||
.forEach((edge) => {
|
||||
let targetNodeId = edge.getTargetCellId()
|
||||
let targetPortId = edge.getTargetPortId()
|
||||
const targetNode: X6DemoNode = edge.getTargetCell()! as X6DemoNode
|
||||
if (targetNode instanceof X6DemoGroupNode) {
|
||||
experimentGraph.deleteEdges(edge)
|
||||
return
|
||||
}
|
||||
const targetParentNode: X6DemoGroupNode | null =
|
||||
targetNode.parent as X6DemoGroupNode
|
||||
// 若源节点不可见,且存在父群组节点,则说明源节点的父群组节点已折叠,此时创建一个连接折叠父节点的桩
|
||||
if (
|
||||
!targetNode.visible &&
|
||||
targetParentNode instanceof X6DemoGroupNode
|
||||
) {
|
||||
targetNodeId = targetParentNode.id
|
||||
const parentNodePorts = targetParentNode.getPortsByGroup('in')
|
||||
targetPortId = parentNodePorts[0].id
|
||||
}
|
||||
graph!.addEdge(
|
||||
new X6DemoGroupEdge({
|
||||
sourceAnchor: 'bottom',
|
||||
source: {
|
||||
cell: node!.id,
|
||||
port: outputPortId,
|
||||
anchor: {
|
||||
name: 'bottom',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
cell: targetNodeId,
|
||||
port: targetPortId,
|
||||
anchor: {
|
||||
name: 'center',
|
||||
},
|
||||
},
|
||||
zIndex: 1,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}, [node, experimentGraph])
|
||||
|
||||
// 展开
|
||||
const onExpandGroup = useCallback(() => {
|
||||
const { graph } = experimentGraph
|
||||
const children = node!.getDescendants()
|
||||
const childNodes: X6DemoNode[] = children.filter((child) =>
|
||||
child.isNode(),
|
||||
) as any[]
|
||||
const { x, y, width, height } = calcNodeScale(
|
||||
{ isCollapsed: false } as any,
|
||||
children.filter((i) => i.isNode()).map((i) => i.getData()),
|
||||
)
|
||||
// 还原尺寸
|
||||
node!.setProp({
|
||||
position: { x, y },
|
||||
size: { width, height },
|
||||
})
|
||||
|
||||
// 处理 Data
|
||||
const prevData = node!.getData<object>()
|
||||
node?.setData({ ...prevData, isCollapsed: false })
|
||||
|
||||
// 显示子元素
|
||||
childNodes.forEach((child) => {
|
||||
child.show()
|
||||
child.updateData({ hide: false })
|
||||
})
|
||||
|
||||
// 删除桩和边
|
||||
const groupEdges = graph?.getConnectedEdges(node!)
|
||||
if (groupEdges?.length) {
|
||||
experimentGraph.deleteEdges(groupEdges)
|
||||
}
|
||||
const ports = node!.getPorts()
|
||||
if (ports?.length) {
|
||||
node!.removePorts(ports)
|
||||
}
|
||||
|
||||
// 建立与外部折叠群组的连接
|
||||
const childIncomingEdges = childNodes.reduce(
|
||||
(accu: Edge[], curr: X6DemoNode) => {
|
||||
const incomingEdgs = graph!.getIncomingEdges(curr)
|
||||
if (incomingEdgs?.length) {
|
||||
return [...accu, ...incomingEdgs]
|
||||
}
|
||||
return accu
|
||||
},
|
||||
[] as Edge[],
|
||||
)
|
||||
const childOutgoingEdges = childNodes.reduce(
|
||||
(accu: Edge[], curr: X6DemoNode) => {
|
||||
const outgoingEdgs = graph!.getOutgoingEdges(curr)
|
||||
if (outgoingEdgs?.length) {
|
||||
return [...accu, ...outgoingEdgs]
|
||||
}
|
||||
return accu
|
||||
},
|
||||
[] as Edge[],
|
||||
)
|
||||
if (childIncomingEdges?.length) {
|
||||
childIncomingEdges
|
||||
.filter(
|
||||
(edge) =>
|
||||
!childNodes.map((i) => i.id).includes(edge.getSourceCellId()),
|
||||
) // 只考虑从外部连接到组里的连线
|
||||
.forEach((edge) => {
|
||||
const sourceNode: X6DemoNode = edge.getSourceCell()! as X6DemoNode
|
||||
if (sourceNode instanceof X6DemoGroupNode) {
|
||||
experimentGraph.deleteEdges(edge)
|
||||
return
|
||||
}
|
||||
const sourceParentNode: X6DemoGroupNode | null =
|
||||
sourceNode.parent as X6DemoGroupNode
|
||||
// 若源节点不可见,且存在父群组节点,则说明源节点的父群组节点已折叠,此时创建一个连接折叠父节点的桩
|
||||
if (
|
||||
!sourceNode.visible &&
|
||||
sourceParentNode instanceof X6DemoGroupNode
|
||||
) {
|
||||
const sourceNodeId = sourceParentNode.id
|
||||
const parentNodePorts = sourceParentNode.getPortsByGroup('out')
|
||||
const sourcePortId = parentNodePorts[0].id
|
||||
const targetNodeId = edge.getTargetCellId()
|
||||
const targetPortId = edge.getTargetPortId()
|
||||
graph!.addEdge(
|
||||
new X6DemoGroupEdge({
|
||||
sourceAnchor: 'bottom',
|
||||
source: {
|
||||
cell: sourceNodeId,
|
||||
port: sourcePortId,
|
||||
anchor: {
|
||||
name: 'bottom',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
cell: targetNodeId,
|
||||
port: targetPortId,
|
||||
anchor: {
|
||||
name: 'center',
|
||||
},
|
||||
},
|
||||
zIndex: 1,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (childOutgoingEdges?.length) {
|
||||
childOutgoingEdges
|
||||
.filter(
|
||||
(edge) =>
|
||||
!childNodes.map((i) => i.id).includes(edge.getTargetCellId()),
|
||||
)
|
||||
.forEach((edge) => {
|
||||
const targetNode: X6DemoNode = edge.getTargetCell()! as X6DemoNode
|
||||
if (targetNode instanceof X6DemoGroupNode) {
|
||||
experimentGraph.deleteEdges(edge)
|
||||
return
|
||||
}
|
||||
const targetParentNode: X6DemoGroupNode | null =
|
||||
targetNode.parent as X6DemoGroupNode
|
||||
// 若源节点不可见,且存在父群组节点,则说明源节点的父群组节点已折叠,此时创建一个连接折叠父节点的桩
|
||||
if (
|
||||
!targetNode.visible &&
|
||||
targetParentNode instanceof X6DemoGroupNode
|
||||
) {
|
||||
const targetNodeId = targetParentNode.id
|
||||
const parentNodePorts = targetParentNode.getPortsByGroup('in')
|
||||
const targetPortId = parentNodePorts[0].id
|
||||
const sourceNodeId = edge.getSourceCellId()
|
||||
const sourcePortId = edge.getSourcePortId()
|
||||
graph!.addEdge(
|
||||
new X6DemoGroupEdge({
|
||||
sourceAnchor: 'bottom',
|
||||
source: {
|
||||
cell: sourceNodeId,
|
||||
port: sourcePortId,
|
||||
anchor: {
|
||||
name: 'bottom',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
cell: targetNodeId,
|
||||
port: targetPortId,
|
||||
anchor: {
|
||||
name: 'center',
|
||||
},
|
||||
},
|
||||
zIndex: 1,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [node, experimentGraph])
|
||||
|
||||
return (
|
||||
<ConfigProvider prefixCls={ANT_PREFIX}>
|
||||
<div className={styles.nodeGroup}>
|
||||
<div className={styles.row}>
|
||||
<Popover
|
||||
content={`组名: ${name}`}
|
||||
overlayClassName={styles.namePopover}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name.length > 20 ? `${name.slice(0, 20)}...` : name}
|
||||
</div>
|
||||
</Popover>
|
||||
{isCollapsed ? (
|
||||
<PlusSquareOutlined
|
||||
className={styles.collapseButton}
|
||||
onClick={onExpandGroup}
|
||||
/>
|
||||
) : (
|
||||
<MinusSquareOutlined
|
||||
className={styles.collapseButton}
|
||||
onClick={onCollapseGroup}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
@import (reference) '~antd/lib/style/themes/default.less';
|
||||
|
||||
.dagContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 40px;
|
||||
|
||||
.canvasContent {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.x6-edge {
|
||||
stroke-width: 1px;
|
||||
&.success {
|
||||
path:nth-child(2) {
|
||||
stroke: #888 !important;
|
||||
}
|
||||
path:nth-child(3) {
|
||||
fill: #888 !important;
|
||||
stroke: #888 !important;
|
||||
}
|
||||
}
|
||||
&.error {
|
||||
stroke-width: 2px;
|
||||
path:nth-child(2) {
|
||||
stroke: rgba(245, 34, 45, 0.45) !important;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
&.edgeProcessing {
|
||||
path:nth-child(2) {
|
||||
stroke: rgba(57, 202, 116, 0.8);
|
||||
stroke-width: 2px;
|
||||
stroke-dasharray: 8px, 2px;
|
||||
&:local {
|
||||
animation: processing-line 30s infinite linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes processing-line {
|
||||
to {
|
||||
stroke-dashoffset: -1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
.x6-split-box-horizontal > .x6-split-box-resizer,
|
||||
.x6-split-box-vertical > .x6-split-box-resizer {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-spin-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.x6-widget-selection-inner {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.x6-widget-selection-box {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import {
|
||||
useExperimentGraph,
|
||||
useUnmountExperimentGraph,
|
||||
} from '@/pages/rx-models/experiment-graph'
|
||||
import { CanvasContent } from './canvas-content'
|
||||
import { CanvasToolbar } from './canvas-toolbar'
|
||||
import { BottomToolbar } from './bottom-toolbar'
|
||||
|
||||
import styles from './index.less'
|
||||
|
||||
interface Props {
|
||||
experimentId: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const DAGCanvas: React.FC<Props> = (props) => {
|
||||
const { experimentId, className } = props
|
||||
const expGraph = useExperimentGraph(experimentId)
|
||||
|
||||
// 处理画布卸载
|
||||
useUnmountExperimentGraph(experimentId)
|
||||
|
||||
// 自定义算法组件的渲染控制
|
||||
useEffect(() => {
|
||||
;(window as any).renderForm = expGraph.setActiveAlgoData
|
||||
return () => {
|
||||
delete (window as any).renderForm
|
||||
}
|
||||
}, [expGraph])
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.dagContainer, className)}>
|
||||
<CanvasToolbar experimentId={experimentId} />
|
||||
<CanvasContent
|
||||
experimentId={experimentId}
|
||||
className={styles.canvasContent}
|
||||
/>
|
||||
<BottomToolbar experimentId={experimentId} />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
@border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
max-height: calc(100vh - 48px);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.experiment {
|
||||
.flex();
|
||||
|
||||
.nodeSourceTree {
|
||||
flex-basis: 290px;
|
||||
width: 290px;
|
||||
max-height: calc(100vh - 48px);
|
||||
border-right: @border;
|
||||
}
|
||||
|
||||
.editPanel {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
|
||||
.dagCanvas {
|
||||
flex-grow: 1;
|
||||
border-right: @border;
|
||||
}
|
||||
|
||||
.confPanel {
|
||||
width: 290px;
|
||||
min-width: 290px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { Layout } from 'antd'
|
||||
import { RouteComponentProps } from 'react-router'
|
||||
import { DndProvider } from 'react-dnd'
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend'
|
||||
import { GuideHeader } from '@/layout/header'
|
||||
import { ComponentTreePanel } from './component-tree-panel'
|
||||
import { ComponentConfigPanel } from './component-config-panel'
|
||||
import { DAGCanvas } from './dag-canvas'
|
||||
|
||||
import styles from './index.less'
|
||||
|
||||
interface Props extends RouteComponentProps<{ experimentId: string }> {
|
||||
experimentId: string
|
||||
}
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
const DagDemo: React.FC<Props> = (props) => {
|
||||
const { experimentId = '1' } = props
|
||||
|
||||
return (
|
||||
<Layout className={styles.layout}>
|
||||
<GuideHeader experimentId={experimentId} />
|
||||
<Content className={styles.content}>
|
||||
<div className={classNames(styles.experiment)}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ComponentTreePanel
|
||||
experimentId={experimentId}
|
||||
className={styles.nodeSourceTree}
|
||||
/>
|
||||
<div className={styles.editPanel}>
|
||||
<DAGCanvas
|
||||
experimentId={experimentId}
|
||||
className={styles.dagCanvas}
|
||||
/>
|
||||
<ComponentConfigPanel
|
||||
experimentId={experimentId}
|
||||
className={styles.confPanel}
|
||||
/>
|
||||
</div>
|
||||
</DndProvider>
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default DagDemo
|
@ -1,817 +0,0 @@
|
||||
/* eslint-disable no-this-assignment */
|
||||
import React, { useEffect } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { ANT_PREFIX } from '@/constants/global'
|
||||
import classnames from 'classnames'
|
||||
import { BehaviorSubject, fromEventPattern, timer, Subscription } from 'rxjs'
|
||||
import { filter, take } from 'rxjs/operators'
|
||||
import { round } from 'lodash-es'
|
||||
import produce from 'immer'
|
||||
import { ConfigProvider, message, Tooltip } from 'antd'
|
||||
import { RERENDER_EVENT } from '@/constants/graph'
|
||||
import { GraphCore, ConnectionRemovedArgs } from './graph-core'
|
||||
import {
|
||||
BaseNode,
|
||||
X6DemoGroupNode,
|
||||
X6DemoNode,
|
||||
} from '../common/graph-common/shape/node'
|
||||
import {
|
||||
BaseEdge,
|
||||
GuideEdge,
|
||||
X6DemoGroupEdge,
|
||||
} from '../common/graph-common/shape/edge'
|
||||
import { NodeElement } from '../dag-canvas/elements/node-element'
|
||||
import { NodeGroup } from '../dag-canvas/elements/node-group'
|
||||
import { NExecutionStatus, NExperiment, NExperimentGraph } from './typing'
|
||||
import {
|
||||
expandGroupAccordingToNodes,
|
||||
formatGraphData,
|
||||
formatNodeInfoToNodeMeta,
|
||||
} from './graph-util'
|
||||
import { queryGraph, addNode, copyNode } from '@/mock/graph'
|
||||
import { queryGraphStatus, runGraph, stopGraphRun } from '@/mock/status'
|
||||
|
||||
export function parseStatus(data: NExecutionStatus.ExecutionStatus) {
|
||||
const { execInfo, instStatus } = data
|
||||
Object.entries(execInfo).forEach(([id, val]) => {
|
||||
// 更新execInfo中的执行状态,后端可能不同步
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
val.jobStatus = instStatus[id]
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
type NodeMeta = ReturnType<typeof formatGraphData>['nodes'][number]
|
||||
|
||||
type EdgeMeta = ReturnType<typeof formatGraphData>['edges'][number]
|
||||
|
||||
interface NodeDataMap {
|
||||
[nodeInstanceId: string]: NExperimentGraph.Node
|
||||
}
|
||||
|
||||
class ExperimentGraph extends GraphCore<BaseNode, BaseEdge> {
|
||||
// 重新声明节点元信息的类型
|
||||
nodeMetas?: NodeMeta[]
|
||||
|
||||
// 重新声明边的元信息的类型
|
||||
edgeMetas?: EdgeMeta[]
|
||||
|
||||
// 等待渲染的节点,由于初次渲染 group 时需要 group 内的节点和边都渲染完成,因此放到 afterLayout 里面渲染 group
|
||||
pendingNodes: BaseNode[] = []
|
||||
|
||||
// 实验 id
|
||||
experimentId: string
|
||||
|
||||
// 实验图加载状态
|
||||
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false)
|
||||
|
||||
// 实验图运行状态
|
||||
running$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false)
|
||||
|
||||
// 实验数据
|
||||
experiment$: BehaviorSubject<NExperiment.Experiment> =
|
||||
new BehaviorSubject<NExperiment.Experiment>(null as any)
|
||||
|
||||
// 实验图数据
|
||||
experimentGraph$: BehaviorSubject<NExperimentGraph.ExperimentGraph> =
|
||||
new BehaviorSubject<NExperimentGraph.ExperimentGraph>(null as any)
|
||||
|
||||
// 当前选中节点
|
||||
activeNodeInstance$: BehaviorSubject<NExecutionStatus.ActiveNode> =
|
||||
new BehaviorSubject<NExecutionStatus.ActiveNode>(null as any)
|
||||
|
||||
// 当前执行状态
|
||||
executionStatus$: BehaviorSubject<NExecutionStatus.ExecutionStatus> =
|
||||
new BehaviorSubject<NExecutionStatus.ExecutionStatus>(null as any)
|
||||
|
||||
// 当前弹窗
|
||||
activeModal$: BehaviorSubject<NExperimentGraph.ModalParams | undefined> =
|
||||
new BehaviorSubject<NExperimentGraph.ModalParams | undefined>(null as any)
|
||||
|
||||
// 当前选中的群组
|
||||
selectedGroup$: BehaviorSubject<X6DemoGroupNode | undefined> =
|
||||
new BehaviorSubject<X6DemoGroupNode | undefined>(undefined)
|
||||
|
||||
// 图数据的订阅
|
||||
experimentGraphSub?: Subscription
|
||||
|
||||
// 查询执行状态的定时器订阅
|
||||
executionStatusQuerySub?: Subscription
|
||||
|
||||
// 主动触发的重新渲染订阅
|
||||
reRenderSub?: Subscription
|
||||
|
||||
constructor(expId: string) {
|
||||
super({
|
||||
history: true,
|
||||
frozen: true,
|
||||
selecting: {
|
||||
enabled: true,
|
||||
rubberband: false,
|
||||
multiple: true,
|
||||
strict: true,
|
||||
showNodeSelectionBox: false,
|
||||
selectNodeOnMoved: false,
|
||||
},
|
||||
keyboard: {
|
||||
enabled: true,
|
||||
},
|
||||
connecting: {
|
||||
snap: { radius: 10 },
|
||||
allowBlank: false,
|
||||
highlight: true,
|
||||
connector: 'pai',
|
||||
sourceAnchor: 'bottom',
|
||||
targetAnchor: 'center',
|
||||
connectionPoint: 'anchor',
|
||||
createEdge() {
|
||||
return new GuideEdge({
|
||||
attrs: {
|
||||
line: {
|
||||
strokeDasharray: '5 5',
|
||||
stroke: '#808080',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
args: {
|
||||
size: '6',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
validateEdge: (args) => {
|
||||
const { edge } = args
|
||||
return !!(edge?.target as any)?.port
|
||||
},
|
||||
// 是否触发交互事件
|
||||
validateMagnet({ magnet }) {
|
||||
return magnet.getAttribute('port-group') !== 'in'
|
||||
},
|
||||
// 显示可用的链接桩
|
||||
validateConnection({
|
||||
sourceView,
|
||||
targetView,
|
||||
sourceMagnet,
|
||||
targetMagnet,
|
||||
}) {
|
||||
// 不允许连接到自己
|
||||
if (sourceView === targetView) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只能从输出链接桩创建连接
|
||||
if (
|
||||
!sourceMagnet ||
|
||||
sourceMagnet.getAttribute('port-group') === 'in'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只能连接到输入链接桩
|
||||
if (
|
||||
!targetMagnet ||
|
||||
targetMagnet.getAttribute('port-group') !== 'in'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 判断目标链接桩是否可连接
|
||||
const portId = targetMagnet.getAttribute('port')!
|
||||
const node = targetView!.cell as X6DemoNode
|
||||
const port = node.getPort(portId)
|
||||
return !(port && port.connected)
|
||||
},
|
||||
},
|
||||
scroller: {
|
||||
enabled: true,
|
||||
pageVisible: false,
|
||||
pageBreak: false,
|
||||
pannable: true,
|
||||
},
|
||||
highlighting: {
|
||||
nodeAvailable: {
|
||||
name: 'className',
|
||||
args: {
|
||||
className: 'available',
|
||||
},
|
||||
},
|
||||
magnetAvailable: {
|
||||
name: 'className',
|
||||
args: {
|
||||
className: 'available',
|
||||
},
|
||||
},
|
||||
magnetAdsorbed: {
|
||||
name: 'className',
|
||||
args: {
|
||||
className: 'adsorbed',
|
||||
},
|
||||
},
|
||||
},
|
||||
onPortRendered(args) {
|
||||
const { port } = args
|
||||
const { contentSelectors } = args
|
||||
const container = contentSelectors && contentSelectors.content
|
||||
|
||||
const placement = port.group === 'in' ? 'top' : 'bottom'
|
||||
|
||||
if (container) {
|
||||
ReactDOM.render(
|
||||
(
|
||||
<ConfigProvider prefixCls={ANT_PREFIX}>
|
||||
<Tooltip
|
||||
title={(port as any).description}
|
||||
placement={placement}
|
||||
>
|
||||
<span
|
||||
className={classnames('ais-port', {
|
||||
connected: (port as any).connected,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ConfigProvider>
|
||||
) as any,
|
||||
container as any,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
this.experimentId = expId
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
// 获取实验和图及执行状态信息
|
||||
async initialize() {
|
||||
// eslint-disable-next-line: no-this-assignment
|
||||
const { experimentId } = this
|
||||
this.loading$.next(true)
|
||||
try {
|
||||
await this.loadExperiment(experimentId)
|
||||
await this.loadExperimentGraph(experimentId)
|
||||
await this.loadExecutionStatus(experimentId)
|
||||
this.loading$.next(false)
|
||||
} catch (e) {
|
||||
this.loading$.next(false)
|
||||
console.error('加载实验错误', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换实验
|
||||
async changeExperiment(id: string) {
|
||||
this.experimentId = id
|
||||
await this.initialize()
|
||||
}
|
||||
|
||||
// 获取实验
|
||||
async loadExperiment(experimentId: string) {
|
||||
try {
|
||||
const res = {
|
||||
projectName: 'sre_mpi_algo_dev',
|
||||
gmtCreate: '2020-08-18 02:21:41',
|
||||
description: '用户流失数据建模demo',
|
||||
name: '建模流程 DEMO',
|
||||
id: 353355,
|
||||
}
|
||||
this.experiment$.next(res)
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
console.error('加载实验错误', e)
|
||||
return { success: false } as any
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图
|
||||
async loadExperimentGraph(experimentId: string) {
|
||||
const graphRes = await queryGraph(experimentId)
|
||||
this.experimentGraph$.next(graphRes.data as any)
|
||||
}
|
||||
// 更新图元
|
||||
async updateExperimentGraph(
|
||||
nodes: NExperimentGraph.Node[] = [],
|
||||
links: NExperimentGraph.Link[] = [],
|
||||
) {
|
||||
const oldGraph = this.experimentGraph$.getValue()
|
||||
const newGraph = produce(oldGraph, (nextGraph: any) => {
|
||||
if (nodes.length) {
|
||||
nextGraph.nodes.push(...nodes)
|
||||
}
|
||||
if (links.length) {
|
||||
nextGraph.links.push(...links)
|
||||
}
|
||||
})
|
||||
this.experimentGraph$.next(newGraph as any)
|
||||
}
|
||||
// 删除图元
|
||||
async delExperimentGraphElement(
|
||||
nodes: string[] = [],
|
||||
links: NExperimentGraph.Link[] = [],
|
||||
) {
|
||||
const oldGraph = this.experimentGraph$.getValue()
|
||||
const newGraph = produce(oldGraph, (nextGraph: any) => {
|
||||
if (nodes.length) {
|
||||
nextGraph.nodes = oldGraph.nodes.filter(
|
||||
(node) => !nodes.includes(node.id.toString()),
|
||||
)
|
||||
nextGraph.links = oldGraph.links.filter(
|
||||
(link) =>
|
||||
!nodes.find((node) =>
|
||||
[link.source.toString(), link.target.toString()].includes(node),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
nextGraph.links = oldGraph.links.filter((link) => {
|
||||
return !links.find((delLink) => {
|
||||
return (
|
||||
delLink.inputPortId.toString() === link.inputPortId.toString() &&
|
||||
delLink.outputPortId.toString() === link.outputPortId.toString()
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
this.experimentGraph$.next(newGraph as any)
|
||||
}
|
||||
|
||||
// 获取执行状态
|
||||
loadExecutionStatus = async (experimentId: string) => {
|
||||
this.executionStatusQuerySub?.unsubscribe()
|
||||
// 每三秒查询一次执行状态
|
||||
this.executionStatusQuerySub = timer(0, 5000).subscribe(
|
||||
async (resPromise) => {
|
||||
const execStatusRes = await queryGraphStatus()
|
||||
this.executionStatus$.next(execStatusRes.data as any)
|
||||
this.updateEdgeStatus()
|
||||
// 执行完成时停止查询状态
|
||||
const { status } = execStatusRes.data
|
||||
if (status === 'default') {
|
||||
this.running$.next(false)
|
||||
this.executionStatusQuerySub?.unsubscribe()
|
||||
} else {
|
||||
this.running$.next(true)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 判断画布是否准备完成(主要用于 react 组件中)
|
||||
isGraphReady() {
|
||||
return !!this.graph
|
||||
}
|
||||
|
||||
// 渲染画布
|
||||
renderGraph = (wrapper: HTMLElement, container: HTMLElement) => {
|
||||
this.experimentGraphSub = this.experimentGraph$
|
||||
.pipe(
|
||||
filter((x) => !!x), // 过滤出有效数据
|
||||
take(1), // 只做一次挂载渲染
|
||||
)
|
||||
.subscribe((graphData) => {
|
||||
if (!this.graph) {
|
||||
const { nodes, edges } = formatGraphData(graphData)
|
||||
super.render({
|
||||
wrapper,
|
||||
container,
|
||||
nodes,
|
||||
edges,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 监听主动触发的重新渲染事件,避免从 IDE 返回后画布消失
|
||||
this.reRenderSub = fromEventPattern(
|
||||
(handler) => {
|
||||
window.addEventListener(RERENDER_EVENT, handler)
|
||||
},
|
||||
(handler) => {
|
||||
window.removeEventListener(RERENDER_EVENT, handler)
|
||||
},
|
||||
).subscribe(this.handlerResize as any)
|
||||
}
|
||||
|
||||
renderNode(nodeMeta: NodeMeta): BaseNode | undefined {
|
||||
const { experimentId } = this
|
||||
const { data } = nodeMeta
|
||||
const { type, includedNodes = [] } = data as any
|
||||
if (type === 'node') {
|
||||
const node = this.graph!.addNode(
|
||||
new X6DemoNode({
|
||||
...nodeMeta,
|
||||
shape: 'ais-rect-port',
|
||||
component: <NodeElement experimentId={experimentId} />,
|
||||
}),
|
||||
) as BaseNode
|
||||
if ((nodeMeta.data as any).hide) {
|
||||
this.pendingNodes.push(node)
|
||||
}
|
||||
return node
|
||||
}
|
||||
if (type === 'group' && includedNodes?.length) {
|
||||
const group = this.graph!.addNode(
|
||||
new X6DemoGroupNode({
|
||||
...nodeMeta,
|
||||
shape: 'react-shape',
|
||||
component: <NodeGroup experimentId={experimentId} />,
|
||||
}),
|
||||
) as BaseNode
|
||||
includedNodes.forEach((normalNode: any) => {
|
||||
const targetNode = this.getNodeById(normalNode.id)
|
||||
group.addChild(targetNode!)
|
||||
})
|
||||
return group
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
afterLayout() {
|
||||
super.afterLayout()
|
||||
this.pendingNodes.forEach((node) => {
|
||||
node.hide()
|
||||
})
|
||||
this.pendingNodes = []
|
||||
}
|
||||
|
||||
renderEdge(edgeMeta: EdgeMeta): BaseEdge | undefined {
|
||||
const { type } = edgeMeta
|
||||
if (type === 'group') {
|
||||
return this.graph!.addEdge(new X6DemoGroupEdge(edgeMeta)) as BaseEdge
|
||||
}
|
||||
return this.graph!.addEdge(new GuideEdge(edgeMeta)) as BaseEdge
|
||||
}
|
||||
|
||||
validateContextMenu = (info: NExperimentGraph.ContextMenuInfo): boolean => {
|
||||
return !(
|
||||
info.type === 'edge' && (info?.data?.edge as BaseEdge)?.isGroupEdge()
|
||||
)
|
||||
}
|
||||
|
||||
onSelectNodes(nodes: BaseNode[]) {
|
||||
const selectedNodes: X6DemoNode[] = nodes.filter(
|
||||
(cell) => cell.isNode() && !cell.isGroup(),
|
||||
) as X6DemoNode[]
|
||||
const selectedGroups: X6DemoGroupNode[] = nodes.filter(
|
||||
(cell) => cell.isNode() && cell.isGroup(),
|
||||
)
|
||||
if (selectedNodes.length === 1) {
|
||||
// 当只选中了一个节点时,激活当前选中项
|
||||
const cell = selectedNodes[0]
|
||||
const nodeData = cell.getData()
|
||||
const currentActiveNode = this.activeNodeInstance$.getValue()
|
||||
if (currentActiveNode?.id !== (nodeData as any)?.id) {
|
||||
this.activeNodeInstance$.next(nodeData as any)
|
||||
}
|
||||
} else {
|
||||
this.selectedNodes$.next(selectedNodes)
|
||||
this.activeNodeInstance$.next(null as any) // 当没选中任何东西时,清空选中的节点信息
|
||||
}
|
||||
if (selectedGroups.length === 1) {
|
||||
this.selectedGroup$.next(selectedGroups[0])
|
||||
} else {
|
||||
this.selectedGroup$.next(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
handlerResize = (e: CustomEvent<string>) => {
|
||||
if (e.detail === this.experimentId) {
|
||||
this.resizeGraph()
|
||||
}
|
||||
}
|
||||
|
||||
async onConnectNode(args: any) {
|
||||
const { edge = {}, isNew } = args
|
||||
const { source, target } = edge as any
|
||||
if (isNew) {
|
||||
// 处理边虚线样式更新的问题。
|
||||
const node = args.currentCell as BaseNode
|
||||
const portId = edge.getTargetPortId()
|
||||
if (node && portId) {
|
||||
// 触发 port 重新渲染
|
||||
node.setPortProp(portId, 'connected', true)
|
||||
// 更新连线样式
|
||||
edge.attr({
|
||||
line: {
|
||||
strokeDasharray: '',
|
||||
targetMarker: '',
|
||||
stroke: '#808080',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
source: source.cell,
|
||||
target: target.cell,
|
||||
outputPortId: source.port,
|
||||
inputPortId: target.port,
|
||||
}
|
||||
edge.setData(data)
|
||||
this.updateExperimentGraph([], [data])
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onConnectionRemoved(args: ConnectionRemovedArgs) {
|
||||
try {
|
||||
const { edge } = args
|
||||
const { target } = edge
|
||||
const { cell: nodeId, port: portId } = target as any
|
||||
if (nodeId) {
|
||||
const targetCell = this.getNodeById(nodeId)!
|
||||
if (targetCell) {
|
||||
// 触发 port 重新渲染
|
||||
targetCell.setPortProp(portId, 'connected', false)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onMoveNodeStart(args: any) {
|
||||
const { node }: { node: BaseNode } = args
|
||||
const parent = node.getParent()
|
||||
const parentData = parent?.getData<any>()
|
||||
if (parentData && !parentData?.isCollapsed) {
|
||||
expandGroupAccordingToNodes({ moveNodes: [node] })
|
||||
}
|
||||
}
|
||||
|
||||
async onMoveNodes(movedNodes: any[]) {
|
||||
const targetNodes = movedNodes.filter((arg) => {
|
||||
const { node } = arg
|
||||
return !node.isGroup()
|
||||
})
|
||||
if (targetNodes?.length) {
|
||||
const newPos = targetNodes.map((moveNode: any) => {
|
||||
const { current, node } = moveNode
|
||||
const { x, y } = current
|
||||
const { id } = node
|
||||
this.updateNodeById(id, (node?: BaseNode) => {
|
||||
node!.setData({ x, y })
|
||||
})
|
||||
return {
|
||||
nodeInstanceId: id,
|
||||
posX: round(x),
|
||||
posY: round(y),
|
||||
}
|
||||
})
|
||||
const oldGraph = this.experimentGraph$.getValue()
|
||||
const newGraph = produce(oldGraph, (nextGraph: any) => {
|
||||
newPos.forEach((position) => {
|
||||
const { nodeInstanceId, posX, posY } = position
|
||||
const matchNode = nextGraph.nodes.find(
|
||||
(item: any) => item.id.toString() === nodeInstanceId.toString(),
|
||||
)
|
||||
if (matchNode) {
|
||||
matchNode.positionX = posX
|
||||
matchNode.positionY = posY
|
||||
}
|
||||
})
|
||||
})
|
||||
this.experimentGraph$.next(newGraph)
|
||||
}
|
||||
}
|
||||
|
||||
onDeleteNodeOrEdge(args: { nodes: BaseNode[]; edges: GuideEdge[] }) {
|
||||
const { nodes, edges } = args
|
||||
const normalNodes: X6DemoNode[] = nodes.filter(
|
||||
(node) => !node.isGroup(),
|
||||
) as X6DemoNode[]
|
||||
if (normalNodes?.length) {
|
||||
this.requestDeleteNodes(normalNodes.map((node) => node.id))
|
||||
}
|
||||
if (edges?.length) {
|
||||
this.requestDeleteEdges(edges)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
validateNodeCopyable(cell: BaseNode) {
|
||||
return cell?.isNode() && !cell!.isGroup()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
onCopyNode(node: X6DemoNode) {
|
||||
try {
|
||||
const nodeData = node.getData<any>()
|
||||
const res = copyNode(nodeData)
|
||||
const newNode = formatNodeInfoToNodeMeta(res as any)
|
||||
this.addNode(newNode)
|
||||
this.clearContextMenuInfo()
|
||||
} catch (error) {
|
||||
message.error('复制节点失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新边的执行状态
|
||||
updateEdgeStatus = () => {
|
||||
if (this.graph) {
|
||||
const { graph } = this
|
||||
const executionStatus = this.executionStatus$.getValue()
|
||||
const { instStatus } = executionStatus
|
||||
const nodeIds = Object.keys(instStatus)
|
||||
const runningNodeIds = nodeIds
|
||||
.filter((id) => instStatus[id] === 'running')
|
||||
.map((i) => i.toString())
|
||||
this.updateEdges((edges) => {
|
||||
edges.forEach((edge) => {
|
||||
const {
|
||||
target: { cell: nodeId },
|
||||
id,
|
||||
} = edge as any
|
||||
const view = graph?.findViewByCell(id)
|
||||
if (!view) {
|
||||
return
|
||||
}
|
||||
if (runningNodeIds.includes(nodeId.toString())) {
|
||||
view!.addClass('edgeProcessing')
|
||||
} else {
|
||||
view!.removeClass('edgeProcessing')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 运行画布或节点
|
||||
runGraph = async () => {
|
||||
try {
|
||||
// eslint-disable-next-line: no-this-assignment
|
||||
const { experimentId, nodeMetas = [] } = this
|
||||
await runGraph(nodeMetas)
|
||||
this.running$.next(true)
|
||||
this.clearContextMenuInfo()
|
||||
this.loadExecutionStatus(experimentId) // 发起执行状态查询
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
console.error(`执行失败`, e)
|
||||
return { success: false }
|
||||
}
|
||||
}
|
||||
|
||||
// 停止实验的执行
|
||||
stopRunGraph = async () => {
|
||||
try {
|
||||
const { experimentId } = this
|
||||
const stopRes = await stopGraphRun()
|
||||
this.running$.next(false)
|
||||
this.clearContextMenuInfo()
|
||||
this.loadExecutionStatus(experimentId) // 发起执行状态查询
|
||||
return stopRes
|
||||
} catch (e) {
|
||||
console.error(`停止失败`, e)
|
||||
return { success: false }
|
||||
}
|
||||
}
|
||||
|
||||
// 设置自定义算法组件节点
|
||||
setActiveAlgoData = (data: any) => {
|
||||
if (!data) {
|
||||
this.activeNodeInstance$.next(null as any)
|
||||
return
|
||||
}
|
||||
const oldData = this.activeNodeInstance$.getValue()
|
||||
this.activeNodeInstance$.next({ ...oldData, ...data }) // 完成两种格式的融合,数据结构更复杂以后,这一句可以变成一个专门的方法
|
||||
}
|
||||
|
||||
// 发起请求增加节点
|
||||
requestAddNode = async (param: {
|
||||
nodeMeta: any
|
||||
clientX: number
|
||||
clientY: number
|
||||
}) => {
|
||||
// eslint-disable-next-line: no-this-assignment
|
||||
const { graph } = this
|
||||
if (graph) {
|
||||
const { nodeMeta, clientX, clientY } = param
|
||||
const pos = graph.clientToLocal(clientX, clientY)
|
||||
const nodeRes = await addNode({ ...nodeMeta, ...pos })
|
||||
this.updateExperimentGraph([nodeRes])
|
||||
const newNode = formatNodeInfoToNodeMeta(nodeRes as any)
|
||||
this.addNode(newNode)
|
||||
return { success: true }
|
||||
}
|
||||
return { success: false } as any
|
||||
}
|
||||
|
||||
// 发起请求删除节点
|
||||
requestDeleteNodes = async (ids: string[] | string) => {
|
||||
const nodeInstanceIds = ([] as string[]).concat(ids)
|
||||
if (this.graph && nodeInstanceIds.length) {
|
||||
this.deleteNodes(nodeInstanceIds)
|
||||
this.clearContextMenuInfo()
|
||||
// 如果被选中节点中包含当前打开的配置面板的节点,则取消激活
|
||||
const activeNodeInstance = this.activeNodeInstance$.getValue()
|
||||
if (
|
||||
activeNodeInstance &&
|
||||
nodeInstanceIds
|
||||
.map((i) => i.toString())
|
||||
.includes(activeNodeInstance.id.toString())
|
||||
) {
|
||||
this.activeNodeInstance$.next(null as any)
|
||||
}
|
||||
this.delExperimentGraphElement(nodeInstanceIds, [])
|
||||
return { success: true }
|
||||
}
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
// 发起请求删除边
|
||||
requestDeleteEdges = async (edges: BaseEdge | BaseEdge[]) => {
|
||||
const targetEdges: BaseEdge[] = ([] as any[]).concat(edges)
|
||||
console.log(targetEdges)
|
||||
this.deleteEdges(targetEdges)
|
||||
this.delExperimentGraphElement(
|
||||
[],
|
||||
targetEdges.map((cell) => cell.getData()),
|
||||
)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// 撤销删除节点
|
||||
undoDeleteNode = async () => {
|
||||
this.undo()
|
||||
}
|
||||
|
||||
// 重命名节点
|
||||
renameNode = async (nodeInstanceId: string, newName: string) => {
|
||||
const renameRes = await { success: true }
|
||||
if (renameRes.success) {
|
||||
const cell = this.getCellById(nodeInstanceId)
|
||||
const data: object = cell!.getData()
|
||||
const newData = { ...data, name: newName }
|
||||
cell!.setData(newData)
|
||||
this.updateExperimentGraph([newData as any])
|
||||
}
|
||||
return renameRes
|
||||
}
|
||||
|
||||
// 缩放特定比例
|
||||
zoomGraph = (factor: number) => {
|
||||
this.zoom(factor)
|
||||
}
|
||||
|
||||
// 缩放到适应画布
|
||||
zoomGraphToFit = () => {
|
||||
this.zoom('fit')
|
||||
}
|
||||
|
||||
// 缩放到实际尺寸
|
||||
zoomGraphRealSize = () => {
|
||||
this.zoom('real')
|
||||
}
|
||||
|
||||
// 从右键菜单删除边
|
||||
deleteEdgeFromContextMenu = async (edge: BaseEdge) => {
|
||||
await this.requestDeleteEdges(edge)
|
||||
this.clearContextMenuInfo()
|
||||
}
|
||||
|
||||
// 清除选中节点
|
||||
unSelectNode = () => {
|
||||
const { graph } = this
|
||||
if (graph) {
|
||||
graph.cleanSelection()
|
||||
}
|
||||
this.selectedGroup$.next(null as any)
|
||||
this.selectedNodes$.next([])
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async setModal(params: NExperimentGraph.ModalParams | undefined) {
|
||||
this.activeModal$.next(params)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.experimentGraphSub?.unsubscribe()
|
||||
this.executionStatusQuerySub?.unsubscribe()
|
||||
this.reRenderSub?.unsubscribe()
|
||||
super.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
export const gModelMap = new Map<string, ExperimentGraph>() // 存储实验图的 model
|
||||
|
||||
export const useExperimentGraph = (experimentId: number | string) => {
|
||||
const expId = experimentId.toString()
|
||||
let existedExperimentGraph = gModelMap.get(expId)
|
||||
if (!existedExperimentGraph) {
|
||||
existedExperimentGraph = new ExperimentGraph(expId)
|
||||
gModelMap.set(expId, existedExperimentGraph)
|
||||
}
|
||||
return existedExperimentGraph
|
||||
}
|
||||
|
||||
export const useUnmountExperimentGraph = (experimentId: string) => {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const existedExperimentGraph = gModelMap.get(experimentId)
|
||||
if (existedExperimentGraph) {
|
||||
existedExperimentGraph.dispose()
|
||||
gModelMap.delete(experimentId)
|
||||
}
|
||||
}
|
||||
}, [experimentId])
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.@{ant-prefix}-spin-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.x6-widget-selection-inner {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.x6-widget-selection-box {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
@ -1,668 +0,0 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import { Graph, Cell, Node, Edge } from '@antv/x6'
|
||||
import { Collection } from '@antv/x6/es/model/collection'
|
||||
import { BehaviorSubject, fromEventPattern, Subscription, merge } from 'rxjs'
|
||||
import { debounceTime, map, tap, scan } from 'rxjs/operators'
|
||||
import './graph-core.less'
|
||||
|
||||
type X6GraphOptions = ConstructorParameters<typeof Graph>[0]
|
||||
|
||||
export type ConnectionRemovedArgs = Collection.EdgeEventArgs['edge:removed']
|
||||
|
||||
// 渲染画布参数
|
||||
interface RenderParams {
|
||||
wrapper?: HTMLElement // 外层容器
|
||||
container?: HTMLElement // 画布容器
|
||||
nodes?: Node.Metadata[]
|
||||
edges?: Node.Metadata[]
|
||||
}
|
||||
|
||||
interface Options extends X6GraphOptions {
|
||||
wrapper?: HTMLElement
|
||||
container?: HTMLElement
|
||||
nodes?: any[]
|
||||
edges?: any[]
|
||||
}
|
||||
|
||||
interface ContextMenuInfo {
|
||||
type: 'edge' | 'graph' | 'node'
|
||||
data: any
|
||||
}
|
||||
|
||||
function setCellsSelectedStatus(cells: Cell[], selected: boolean) {
|
||||
cells.forEach((cell) => {
|
||||
const data = cell.getData<object>() || {}
|
||||
cell.setData({ ...data, selected })
|
||||
})
|
||||
}
|
||||
|
||||
export class GraphCore<
|
||||
N extends Node<Node.Properties> = Node<Node.Properties>,
|
||||
E extends Edge<Edge.Properties> = Edge<Edge.Properties>,
|
||||
> {
|
||||
wrapper?: HTMLElement
|
||||
|
||||
container?: HTMLElement
|
||||
|
||||
nodeMetas?: any[] // 传入的 nodes 原始信息
|
||||
|
||||
edgeMetas?: any[] // 传入的 edges 原始信息
|
||||
|
||||
options: Exclude<Options, 'wrapper' | 'nodes' | 'edges'>
|
||||
|
||||
graph?: Graph
|
||||
|
||||
// 当前画布右键点击信息
|
||||
contextMenuInfo$: BehaviorSubject<ContextMenuInfo> =
|
||||
new BehaviorSubject<ContextMenuInfo>(null as any)
|
||||
|
||||
// 选中的节点
|
||||
selectedNodes$: BehaviorSubject<N[]> = new BehaviorSubject<N[]>([])
|
||||
|
||||
// 待复制的节点 id
|
||||
copyableNodeId$: BehaviorSubject<string> = new BehaviorSubject<string>('')
|
||||
|
||||
// 窗口大小 resize 的订阅
|
||||
private windowResizeSub?: Subscription
|
||||
|
||||
// 右键菜单的订阅
|
||||
private contextMenuSub?: Subscription
|
||||
|
||||
// 节点右键菜单的订阅
|
||||
private nodeContextMenuSub?: Subscription
|
||||
|
||||
// 选中节点的订阅
|
||||
private selectNodeSub?: Subscription
|
||||
|
||||
// 产生连线的订阅
|
||||
private connectNodeSub?: Subscription
|
||||
|
||||
// 连线已删除的订阅
|
||||
private connectionRemovedSub?: Subscription
|
||||
|
||||
// 节点移动的订阅
|
||||
private moveNodesSub?: Subscription
|
||||
|
||||
// 删除节点或连线的订阅
|
||||
private deleteNodeOrEdgeSub?: Subscription
|
||||
|
||||
// 复制节点的订阅
|
||||
private copyNodeSub?: Subscription
|
||||
|
||||
constructor(options: Options) {
|
||||
const { wrapper, container, nodes, edges, ...others } = options
|
||||
this.setMeta(options)
|
||||
this.options = others
|
||||
}
|
||||
|
||||
setMeta(params: Pick<Options, 'wrapper' | 'container' | 'nodes' | 'edges'>) {
|
||||
const { wrapper, container, nodes, edges } = params
|
||||
if (wrapper) {
|
||||
this.setWrapper(wrapper)
|
||||
}
|
||||
if (container) {
|
||||
this.setContainer(container)
|
||||
}
|
||||
if (nodes) {
|
||||
this.setNodes(nodes)
|
||||
}
|
||||
if (edges) {
|
||||
this.setEdges(edges)
|
||||
}
|
||||
}
|
||||
|
||||
get isMetaValid(): boolean {
|
||||
const { wrapper, container, nodeMetas, edgeMetas } = this
|
||||
return !!wrapper && !!container && !!nodeMetas && !!edgeMetas
|
||||
}
|
||||
|
||||
get nodes(): N[] {
|
||||
return (this.graph?.getNodes() || []) as N[]
|
||||
}
|
||||
|
||||
setWrapper(wrapper: HTMLElement) {
|
||||
this.wrapper = wrapper
|
||||
}
|
||||
|
||||
setContainer(container: HTMLElement) {
|
||||
this.container = container
|
||||
this.options.container = container
|
||||
}
|
||||
|
||||
setNodes(nodes: any[]) {
|
||||
this.nodeMetas = nodes
|
||||
}
|
||||
|
||||
setEdges(edges: any[]) {
|
||||
this.edgeMetas = edges
|
||||
}
|
||||
|
||||
// 渲染
|
||||
render(params: RenderParams) {
|
||||
this.setMeta(params)
|
||||
if (this.isMetaValid) {
|
||||
const { wrapper, options, nodeMetas, edgeMetas } = this
|
||||
const width = wrapper!.clientWidth
|
||||
const height = wrapper!.clientHeight
|
||||
const graph = new Graph({ ...options, width, height })
|
||||
this.graph = graph
|
||||
nodeMetas!.forEach((nodeMeta) => this.renderNode(nodeMeta))
|
||||
edgeMetas!.forEach((edgeMeta) => this.renderEdge(edgeMeta))
|
||||
this.afterLayout()
|
||||
if (graph.isFrozen()) {
|
||||
graph.unfreeze()
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
graph.centerContent()
|
||||
})
|
||||
|
||||
// 处理窗口缩放
|
||||
this.windowResizeSub = fromEventPattern(
|
||||
(handler) => {
|
||||
window.addEventListener('resize', handler)
|
||||
},
|
||||
(handler) => {
|
||||
window.removeEventListener('resize', handler)
|
||||
},
|
||||
).subscribe(this.resizeGraph)
|
||||
|
||||
// 处理右键菜单
|
||||
const nodeContextMenuObs = fromEventPattern<ContextMenuInfo>(
|
||||
(handler) => {
|
||||
graph.on('node:contextmenu', (data) => {
|
||||
handler({ type: 'node', data })
|
||||
})
|
||||
},
|
||||
(handler) => {
|
||||
graph.off('node:contextmenu', handler)
|
||||
},
|
||||
)
|
||||
|
||||
const edgeContextMenuObs = fromEventPattern<ContextMenuInfo>(
|
||||
(handler) => {
|
||||
graph.on('edge:contextmenu', (data) => {
|
||||
handler({ type: 'edge', data })
|
||||
})
|
||||
},
|
||||
(handler) => {
|
||||
graph.off('edge:contextmenu', handler)
|
||||
},
|
||||
)
|
||||
const graphContextMenuObs = fromEventPattern<ContextMenuInfo>(
|
||||
(handler) => {
|
||||
graph.on('blank:contextmenu', (data) => {
|
||||
handler({ type: 'graph', data })
|
||||
})
|
||||
},
|
||||
(handler) => {
|
||||
graph.off('edge:contextmenu', handler)
|
||||
},
|
||||
)
|
||||
|
||||
this.nodeContextMenuSub = nodeContextMenuObs.subscribe((data) => {
|
||||
this.onNodeContextMenu(data)
|
||||
})
|
||||
|
||||
this.contextMenuSub = merge<ContextMenuInfo>(
|
||||
nodeContextMenuObs,
|
||||
edgeContextMenuObs,
|
||||
graphContextMenuObs,
|
||||
).subscribe((data) => {
|
||||
if (this.validateContextMenu(data)) {
|
||||
this.contextMenuInfo$.next(data)
|
||||
this.onContextMenu(data)
|
||||
}
|
||||
})
|
||||
|
||||
// 处理节点选中事件
|
||||
this.selectNodeSub = fromEventPattern<{ removed: N[]; selected: N[] }>(
|
||||
(handler) => {
|
||||
graph.on('selection:changed', handler)
|
||||
},
|
||||
(handler) => {
|
||||
graph.off('selection:changed', handler)
|
||||
},
|
||||
).subscribe((args) => {
|
||||
const { removed, selected } = args
|
||||
setCellsSelectedStatus(removed, false)
|
||||
setCellsSelectedStatus(selected, true)
|
||||
this.onSelectNodes(selected)
|
||||
})
|
||||
|
||||
// 处理产生连线事件
|
||||
this.connectNodeSub = fromEventPattern(
|
||||
(handler) => {
|
||||
graph.on('edge:connected', handler)
|
||||
},
|
||||
(handler) => {
|
||||
graph.off('edge:connected', handler)
|
||||
},
|
||||
).subscribe((args) => {
|
||||
this.onConnectNode(args)
|
||||
})
|
||||
|
||||
// 处理连线删除事件
|
||||
this.connectionRemovedSub = fromEventPattern<ConnectionRemovedArgs>(
|
||||
(handler) => {
|
||||
graph.on('edge:removed', handler)
|
||||
},
|
||||
(handler) => {
|
||||
graph.off('edge:removed', handler)
|
||||
},
|
||||
// eslint-disable-next-line consistent-return
|
||||
).subscribe((args) => {
|
||||
this.onConnectionRemoved(args)
|
||||
})
|
||||
|
||||
// 处理节点移动事件
|
||||
let moveStarted: boolean = false // 因为需要对移动事件做分片,区分两次移动事件,所以引入一个记录移动开始的变量
|
||||
this.moveNodesSub = fromEventPattern(
|
||||
(handler) => {
|
||||
graph.on('node:change:position', handler)
|
||||
},
|
||||
(handler) => {
|
||||
graph.off('node:change:position', handler)
|
||||
},
|
||||
)
|
||||
.pipe(
|
||||
tap((args) => {
|
||||
this.onMoveNodeStart(args)
|
||||
}),
|
||||
scan((accum, args: any) => {
|
||||
const currentAccum = !moveStarted ? [] : accum
|
||||
const { node } = args
|
||||
const { id } = node
|
||||
const matchItemIndex = currentAccum.findIndex(
|
||||
(item) => item.id === id,
|
||||
)
|
||||
if (matchItemIndex > -1) {
|
||||
currentAccum.splice(matchItemIndex, 1, { id, data: args })
|
||||
} else {
|
||||
currentAccum.push({ id, data: args })
|
||||
}
|
||||
return currentAccum
|
||||
}, [] as { id: string; data: any }[]),
|
||||
tap(() => {
|
||||
if (!moveStarted) {
|
||||
moveStarted = true
|
||||
}
|
||||
}),
|
||||
debounceTime(300),
|
||||
tap(() => {
|
||||
if (moveStarted) {
|
||||
moveStarted = false
|
||||
}
|
||||
}),
|
||||
map((items) => items.map((item) => item.data)),
|
||||
)
|
||||
.subscribe((movedNodes: any[]) => {
|
||||
this.onMoveNodes(movedNodes)
|
||||
})
|
||||
|
||||
// 处理删除节点或连线事件
|
||||
this.deleteNodeOrEdgeSub = fromEventPattern(
|
||||
(handler) => {
|
||||
graph.bindKey(['delete', 'backspace'], handler)
|
||||
},
|
||||
() => {
|
||||
graph.unbindKey(['delete', 'backspace'])
|
||||
},
|
||||
).subscribe(() => {
|
||||
const selectedCells = graph.getSelectedCells()
|
||||
const selectedNodes = selectedCells.filter((cell) =>
|
||||
cell.isNode(),
|
||||
) as N[]
|
||||
const selectedEdges = selectedCells.filter((cell) =>
|
||||
cell.isEdge(),
|
||||
) as E[]
|
||||
this.onDeleteNodeOrEdge({ nodes: selectedNodes, edges: selectedEdges })
|
||||
})
|
||||
|
||||
// 处理节点复制事件
|
||||
this.copyNodeSub = fromEventPattern(
|
||||
(handler) => {
|
||||
graph.bindKey(['command+c', 'ctrl+c', 'command+v', 'ctrl+v'], handler)
|
||||
},
|
||||
() => {
|
||||
graph.unbindKey(['command+c', 'ctrl+c', 'command+v', 'ctrl+v'])
|
||||
},
|
||||
).subscribe((args: any) => {
|
||||
const [, action] = args
|
||||
const selectedCells: N[] = (graph.getSelectedCells() as N[]).filter(
|
||||
(cell) => this.validateNodeCopyable(cell),
|
||||
)
|
||||
const copyableNodeId = this.copyableNodeId$.getValue()
|
||||
let copyableNode: N
|
||||
if (copyableNodeId) {
|
||||
copyableNode = graph.getCellById(copyableNodeId) as N
|
||||
}
|
||||
switch (action) {
|
||||
case 'command+c':
|
||||
case 'ctrl+c':
|
||||
if (selectedCells?.length) {
|
||||
this.setCopyableNodeId(selectedCells[0].id) // 当前只支持单节点的复制粘贴
|
||||
}
|
||||
break
|
||||
case 'command+v':
|
||||
case 'ctrl+v':
|
||||
// @ts-ignore
|
||||
if (copyableNode) {
|
||||
this.onCopyNode(copyableNode)
|
||||
}
|
||||
break
|
||||
default:
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.throwRenderError()
|
||||
}
|
||||
}
|
||||
|
||||
renderNode(nodeMeta: any): N | undefined {
|
||||
return this.graph!.addNode(nodeMeta) as N
|
||||
}
|
||||
|
||||
renderEdge(edgeMeta: any): E | undefined {
|
||||
return this.graph!.addEdge(edgeMeta) as E
|
||||
}
|
||||
|
||||
afterLayout() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] call afterLayout')
|
||||
}
|
||||
}
|
||||
|
||||
resizeGraph = () => {
|
||||
const { graph, wrapper } = this
|
||||
if (graph && wrapper) {
|
||||
requestAnimationFrame(() => {
|
||||
const width = wrapper.clientWidth
|
||||
const height = wrapper.clientHeight
|
||||
graph.resize(width, height)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
validateContextMenu(data: ContextMenuInfo): boolean {
|
||||
return !!data
|
||||
}
|
||||
|
||||
onContextMenu(data: ContextMenuInfo): any {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] context menu info:', data)
|
||||
}
|
||||
}
|
||||
|
||||
onNodeContextMenu(data: ContextMenuInfo): any {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] context menu info:', data)
|
||||
}
|
||||
}
|
||||
|
||||
onSelectNodes(nodes: N[]) {
|
||||
this.selectedNodes$.next(nodes)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] select nodes:', nodes)
|
||||
}
|
||||
}
|
||||
|
||||
onConnectNode(args: any) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] connect node:', args)
|
||||
}
|
||||
}
|
||||
|
||||
onConnectionRemoved(args: ConnectionRemovedArgs) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] delete connection:', args)
|
||||
}
|
||||
}
|
||||
|
||||
onMoveNodeStart(args: any) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] move node start:', args)
|
||||
}
|
||||
}
|
||||
|
||||
onMoveNodes(args: any[]) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] move nodes:', args)
|
||||
}
|
||||
}
|
||||
|
||||
// 按下删除键的回调,默认参数为当前选中的节点和边
|
||||
onDeleteNodeOrEdge(args: { nodes: N[]; edges: E[] }) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] delete node or edge:', args)
|
||||
}
|
||||
}
|
||||
|
||||
// 校验节点是否可复制,为 true 则可被用于复制
|
||||
validateNodeCopyable(node: N) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] validate node copyable:', node)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 按下粘贴键的回调,默认参数为待复制的节点
|
||||
onCopyNode(copyNode: N) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GraphCore] copy node:', copyNode)
|
||||
}
|
||||
}
|
||||
|
||||
/* 以下为主动触发的方法 */
|
||||
addNode = (nodeMeta: any) => {
|
||||
this.nodeMetas?.push(nodeMeta)
|
||||
return this.renderNode(nodeMeta)
|
||||
}
|
||||
|
||||
addEdge = (edgeMeta: any) => {
|
||||
this.edgeMetas?.push(edgeMeta)
|
||||
return this.renderEdge(edgeMeta)
|
||||
}
|
||||
|
||||
getNodeById = (nodeId: string): N | undefined => {
|
||||
const node = this.graph?.getCellById(nodeId) as N
|
||||
if (node?.isNode()) {
|
||||
return node
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
getNodes = (): N[] => {
|
||||
return (this.graph?.getNodes() as N[]) || []
|
||||
}
|
||||
|
||||
getEdgeById = (nodeId: string): E | undefined => {
|
||||
const edge = this.graph?.getCellById(nodeId) as E
|
||||
if (edge?.isEdge()) {
|
||||
return edge
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
getEdges = (): E[] => {
|
||||
return (this.graph?.getEdges() as E[]) || []
|
||||
}
|
||||
|
||||
getCellById = (cellId: string): N | E | undefined => {
|
||||
const cell = this.graph?.getCellById(cellId) as N | E
|
||||
if (cell?.isNode() || cell?.isEdge()) {
|
||||
return cell
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
getCells = (): (N | E)[] => {
|
||||
return (this.graph?.getCells() as (N | E)[]) || []
|
||||
}
|
||||
|
||||
updateNodeById = (nodeId: string, handler: (node?: N) => void) => {
|
||||
handler(this.getNodeById(nodeId))
|
||||
}
|
||||
|
||||
updateNodes = (handler: (nodes: N[]) => void) => {
|
||||
handler(this.getNodes())
|
||||
}
|
||||
|
||||
updateEdgeById = (edgeId: string, handler: (node?: E) => void) => {
|
||||
const edge = this.graph?.getCellById(edgeId) as E
|
||||
if (edge?.isEdge()) {
|
||||
handler(edge)
|
||||
} else {
|
||||
handler(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
updateEdges = (handler: (edges: E[]) => void) => {
|
||||
const edges = (this.graph?.getEdges() as E[]) || []
|
||||
handler(edges)
|
||||
}
|
||||
|
||||
// 删除节点
|
||||
deleteNodes = (nodes: (Node | string)[] | Node | string) => {
|
||||
const target = ([] as (Node | string)[]).concat(nodes)
|
||||
// @ts-ignore
|
||||
this.nodeMetas = this.nodeMetas.filter(
|
||||
(nodeMeta) => !target.includes(nodeMeta.id),
|
||||
)
|
||||
this.graph?.removeCells(target)
|
||||
}
|
||||
|
||||
// 删除边
|
||||
deleteEdges = (edges: (Edge | string)[] | Edge | string) => {
|
||||
const target = ([] as (Edge | string)[]).concat(edges)
|
||||
const targetIds = target.map((i) => (typeof i === 'string' ? i : i.id))
|
||||
// @ts-ignore
|
||||
this.edgeMetas = this.edgeMetas.filter(
|
||||
(edgeMeta) => !targetIds.includes(edgeMeta.id),
|
||||
)
|
||||
this.graph?.removeCells(target)
|
||||
}
|
||||
|
||||
// 清空右键菜单信息
|
||||
clearContextMenuInfo = () => {
|
||||
this.contextMenuInfo$.next(null as any)
|
||||
}
|
||||
|
||||
// 缩放画布
|
||||
zoom = (factor: number | 'fit' | 'real') => {
|
||||
if (typeof factor === 'number') {
|
||||
this.graph?.zoom(factor)
|
||||
} else if (factor === 'fit') {
|
||||
this.graph?.zoomToFit({ padding: 12 })
|
||||
} else if (factor === 'real') {
|
||||
this.graph?.scale(1)
|
||||
this.graph?.centerContent()
|
||||
}
|
||||
}
|
||||
|
||||
// 切换可框选模式
|
||||
toggleSelectionEnabled = (enabled?: boolean) => {
|
||||
const { graph } = this
|
||||
if (graph) {
|
||||
const needEnableRubberBand: boolean =
|
||||
typeof enabled === 'undefined' ? !graph.isRubberbandEnabled() : enabled
|
||||
if (needEnableRubberBand) {
|
||||
graph.disablePanning()
|
||||
graph.enableRubberband()
|
||||
// graph.scroller.widget?.setCursor('crosshair', { silent: true })
|
||||
} else {
|
||||
graph.enablePanning()
|
||||
graph.disableRubberband()
|
||||
// graph.scroller.widget?.setCursor('grab', { silent: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选中节点
|
||||
selectNodes = <T extends string | number>(ids: T | T[]) => {
|
||||
const { graph } = this
|
||||
if (graph) {
|
||||
const target = ([] as any[]).concat(ids).map((i) => i.toString())
|
||||
graph.cleanSelection()
|
||||
graph.select(target)
|
||||
if (!Array.isArray(ids)) {
|
||||
const cell = graph.getCellById(ids as string)
|
||||
graph.scrollToCell(cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清除选中节点
|
||||
unSelectNode = () => {
|
||||
const { graph } = this
|
||||
if (graph) {
|
||||
graph.cleanSelection()
|
||||
}
|
||||
}
|
||||
|
||||
// redo
|
||||
redo = () => {
|
||||
const { graph } = this
|
||||
if (graph) {
|
||||
graph.redo()
|
||||
}
|
||||
}
|
||||
|
||||
// undo
|
||||
undo = () => {
|
||||
const { graph } = this
|
||||
if (graph) {
|
||||
graph.undo()
|
||||
}
|
||||
}
|
||||
|
||||
// 设置待复制的节点 id
|
||||
setCopyableNodeId = (id: string) => {
|
||||
this.copyableNodeId$.next(id)
|
||||
}
|
||||
|
||||
// 抛出渲染中遇到的阻断
|
||||
throwRenderError = () => {
|
||||
if (!this.wrapper) {
|
||||
throw new Error('Wrapper element is needed.')
|
||||
}
|
||||
if (!this.container) {
|
||||
throw new Error('Container element is needed.')
|
||||
}
|
||||
if (!this.nodeMetas) {
|
||||
throw new Error('NodeMetas could not be empty')
|
||||
}
|
||||
if (!this.edgeMetas) {
|
||||
throw new Error('EdgeMetas could not be empty')
|
||||
}
|
||||
}
|
||||
|
||||
// 注销
|
||||
dispose() {
|
||||
this.windowResizeSub?.unsubscribe()
|
||||
this.contextMenuSub?.unsubscribe()
|
||||
this.nodeContextMenuSub?.unsubscribe()
|
||||
this.selectNodeSub?.unsubscribe()
|
||||
this.connectNodeSub?.unsubscribe()
|
||||
this.connectionRemovedSub?.unsubscribe()
|
||||
this.moveNodesSub?.unsubscribe()
|
||||
this.deleteNodeOrEdgeSub?.unsubscribe()
|
||||
this.copyNodeSub?.unsubscribe()
|
||||
|
||||
// ! 这一步要注意放在图的事件订阅都取消了之后
|
||||
if (this.wrapper) {
|
||||
const graphScroller = this.wrapper.querySelector('.x6-graph-scroller')
|
||||
if (graphScroller) {
|
||||
graphScroller.innerHTML = ''
|
||||
graphScroller.setAttribute('style', '')
|
||||
graphScroller.setAttribute('class', '')
|
||||
|
||||
if (this.container) {
|
||||
this.container.innerHTML = ''
|
||||
this.container.setAttribute('style', '')
|
||||
this.container.setAttribute('class', '')
|
||||
}
|
||||
}
|
||||
this.graph?.dispose()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,321 +0,0 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import { minBy, maxBy, sortBy } from 'lodash-es'
|
||||
import {
|
||||
GROUP_HORIZONTAL__PADDING,
|
||||
GROUP_VERTICAL__PADDING,
|
||||
NODE_WIDTH,
|
||||
NODE_HEIGHT,
|
||||
} from '@/constants/graph'
|
||||
import { NExperimentGraph } from '@/pages/rx-models/typing'
|
||||
import { BaseNode } from '../common/graph-common/shape/node'
|
||||
import '../common/graph-common/connector'
|
||||
|
||||
// group 范围适应内部节点变化
|
||||
export function expandGroupAccordingToNodes(params: { moveNodes: BaseNode[] }) {
|
||||
const { moveNodes } = params
|
||||
const parentNodes: BaseNode[] = []
|
||||
moveNodes.forEach((node: BaseNode) => {
|
||||
const parentNode: BaseNode = node.getParent() as BaseNode
|
||||
if (parentNode && !parentNodes.includes(parentNode)) {
|
||||
parentNodes.push(parentNode)
|
||||
}
|
||||
})
|
||||
parentNodes.forEach((parent) => {
|
||||
const originSize = parent.getSize()
|
||||
const originPosition = parent.getPosition()
|
||||
const originX = originPosition.x
|
||||
const originY = originPosition.y
|
||||
const originWidth = originSize.width
|
||||
const originHeight = originSize.height
|
||||
const children = parent.getChildren() as BaseNode[]
|
||||
const childNodes = children.filter((child) => child.isNode())
|
||||
if (childNodes?.length) {
|
||||
const positions = childNodes.map((childNode) => childNode.getPosition())
|
||||
const minX = minBy(positions, 'x')?.x!
|
||||
const minY = minBy(positions, 'y')?.y!
|
||||
const maxX = maxBy(positions, 'x')?.x!
|
||||
const maxY = maxBy(positions, 'y')?.y!
|
||||
|
||||
const nextX = minX - GROUP_HORIZONTAL__PADDING
|
||||
const nextY = minY - GROUP_VERTICAL__PADDING
|
||||
const nextWidth = maxX - minX + 2 * GROUP_HORIZONTAL__PADDING + NODE_WIDTH
|
||||
const nextHeight = maxY - minY + 2 * GROUP_VERTICAL__PADDING + NODE_HEIGHT
|
||||
if (
|
||||
originX !== nextX ||
|
||||
originY !== nextY ||
|
||||
originWidth !== nextWidth ||
|
||||
originHeight !== nextHeight
|
||||
) {
|
||||
parent.prop({
|
||||
position: { x: nextX, y: nextY },
|
||||
size: { width: nextWidth, height: nextHeight },
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化单个节点,新增节点时复用
|
||||
export function formatNodeInfoToNodeMeta(
|
||||
nodeData: NExperimentGraph.Node,
|
||||
inputPortConnectedMap: { [k: string]: boolean } = {},
|
||||
) {
|
||||
const portItems: any[] = []
|
||||
const { id, nodeInstanceId, positionX, positionY, inPorts, outPorts } =
|
||||
nodeData
|
||||
sortBy(inPorts, 'sequence').forEach((inPort: any) => {
|
||||
portItems.push({
|
||||
...inPort,
|
||||
group: 'in',
|
||||
id: inPort.id.toString(),
|
||||
connected: !!inputPortConnectedMap[inPort.id.toString()],
|
||||
})
|
||||
})
|
||||
sortBy(outPorts, 'sequence').forEach((outPort: any) => {
|
||||
portItems.push({
|
||||
...outPort,
|
||||
group: 'out',
|
||||
id: outPort.id.toString(),
|
||||
connected: !!inputPortConnectedMap[outPort.id.toString()],
|
||||
})
|
||||
})
|
||||
const x = positionX || 0
|
||||
const y = positionY || 0
|
||||
return {
|
||||
...nodeData,
|
||||
x,
|
||||
y,
|
||||
type: 'node',
|
||||
id: (nodeInstanceId || id)!.toString(),
|
||||
width: NODE_WIDTH,
|
||||
height: NODE_HEIGHT,
|
||||
data: {
|
||||
...nodeData,
|
||||
type: 'node',
|
||||
x,
|
||||
y,
|
||||
id: (nodeInstanceId || id)!.toString(),
|
||||
},
|
||||
ports: {
|
||||
items: portItems,
|
||||
},
|
||||
zIndex: 10,
|
||||
}
|
||||
}
|
||||
|
||||
// 根据群组节点的收缩状态及子节点,计算出节点群组的尺寸和位置
|
||||
export function calcNodeScale(
|
||||
groupData: NExperimentGraph.Group,
|
||||
children: any[],
|
||||
) {
|
||||
const { isCollapsed = false } = groupData
|
||||
const minX = minBy(children, 'x')?.x
|
||||
const minY = minBy(children, 'y')?.y
|
||||
const maxX = maxBy(children, 'x')?.x
|
||||
const maxY = maxBy(children, 'y')?.y
|
||||
|
||||
const defaultX = minX - GROUP_HORIZONTAL__PADDING
|
||||
const defaultY = minY - GROUP_VERTICAL__PADDING
|
||||
const defaultWidth = maxX - minX + 2 * GROUP_HORIZONTAL__PADDING + NODE_WIDTH
|
||||
|
||||
if (isCollapsed) {
|
||||
return {
|
||||
x: defaultX + defaultWidth / 2 - NODE_WIDTH / 2,
|
||||
y: defaultY,
|
||||
width: NODE_WIDTH,
|
||||
height: NODE_HEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: defaultX,
|
||||
y: defaultY,
|
||||
width: defaultWidth,
|
||||
height: maxY - minY + 2 * GROUP_VERTICAL__PADDING + NODE_HEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化单个群组,新增群组时复用
|
||||
export function formatGroupInfoToNodeMeta(
|
||||
groupData: NExperimentGraph.Group,
|
||||
nodeDatas: any[],
|
||||
edges?: any[],
|
||||
) {
|
||||
const { id, isCollapsed = false } = groupData
|
||||
const includedNodes: any[] = nodeDatas.filter(
|
||||
(nodeMeta: any) => nodeMeta.groupId.toString() === id.toString(),
|
||||
)
|
||||
|
||||
const { x, y, width, height } = calcNodeScale(groupData, includedNodes)
|
||||
|
||||
// group 已收缩则必定已存在,而不是处理新增的 group,因此处理的 nodeDatas 是 meta 数据而不是 getData 的数据
|
||||
if (isCollapsed && edges) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
includedNodes.forEach((node) => {
|
||||
node.data.hide = true
|
||||
})
|
||||
const includedNodeIds: string[] = includedNodes.map((nodeData) =>
|
||||
nodeData.id.toString(),
|
||||
)
|
||||
// 从外部进入到 group 内部的连线
|
||||
const edgesFromOutside = edges.filter((edge: any) => {
|
||||
const { source, target } = edge
|
||||
return (
|
||||
includedNodeIds.includes(target.cell.toString()) &&
|
||||
!includedNodeIds.includes(source.cell.toString())
|
||||
)
|
||||
})
|
||||
// 从 group 穿到外部的连线
|
||||
const edgesToOutside = edges.filter((edge: any) => {
|
||||
const { source, target } = edge
|
||||
return (
|
||||
includedNodeIds.includes(source.cell.toString()) &&
|
||||
!includedNodeIds.includes(target.cell.toString())
|
||||
)
|
||||
})
|
||||
const portItems = []
|
||||
if (edgesFromOutside?.length) {
|
||||
const portId = Date.now().toString()
|
||||
portItems.push({ group: 'in', id: portId, connected: true }) // 增加 group 的输入桩
|
||||
// 增加连接到 group 输入桩的连线
|
||||
edgesFromOutside.forEach((edge: any) => {
|
||||
const { source, outputPortId } = edge
|
||||
edges.push({
|
||||
sourceAnchor: 'bottom',
|
||||
source: {
|
||||
cell: source.cell.toString(),
|
||||
port: outputPortId.toString(),
|
||||
anchor: {
|
||||
name: 'bottom',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
cell: id.toString(),
|
||||
port: portId,
|
||||
anchor: {
|
||||
name: 'center',
|
||||
},
|
||||
},
|
||||
label: '',
|
||||
zIndex: 1,
|
||||
})
|
||||
})
|
||||
}
|
||||
if (edgesToOutside?.length) {
|
||||
const portId = (Date.now() + 1).toString()
|
||||
portItems.push({ group: 'out', id: portId, connected: false }) // 增加 group 的输出桩
|
||||
// 增加链接到 group 输出桩的连线
|
||||
edgesToOutside.forEach((edge: any) => {
|
||||
const { target, inputPortId } = edge
|
||||
edges.push({
|
||||
type: 'group',
|
||||
sourceAnchor: 'bottom',
|
||||
source: {
|
||||
cell: id,
|
||||
port: portId,
|
||||
anchor: {
|
||||
name: 'bottom',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
cell: target.cell,
|
||||
port: inputPortId,
|
||||
anchor: {
|
||||
name: 'center',
|
||||
},
|
||||
},
|
||||
label: '',
|
||||
zIndex: 1,
|
||||
})
|
||||
})
|
||||
}
|
||||
return {
|
||||
type: 'group',
|
||||
...groupData,
|
||||
id: id.toString(),
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
zIndex: 1,
|
||||
data: { ...groupData, type: 'group', includedNodes, id: id.toString() },
|
||||
ports: {
|
||||
items: portItems,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'group',
|
||||
...groupData,
|
||||
id: id.toString(),
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
zIndex: 1,
|
||||
data: { ...groupData, type: 'group', includedNodes, id: id.toString() },
|
||||
}
|
||||
}
|
||||
|
||||
// 将接口返回的图信息转换为图渲染引擎可渲染的信息
|
||||
export function formatGraphData(
|
||||
graphData: NExperimentGraph.ExperimentGraph = {} as any,
|
||||
) {
|
||||
const { nodes = [], links = [], groups = [] } = graphData
|
||||
|
||||
// 格式化边
|
||||
const formattedEdges = links.map((link: NExperimentGraph.Link) => {
|
||||
const { source, outputPortId, target, inputPortId } = link
|
||||
return {
|
||||
...link,
|
||||
data: { ...link },
|
||||
sourceAnchor: 'bottom',
|
||||
source: {
|
||||
cell: source.toString(),
|
||||
port: outputPortId.toString(),
|
||||
anchor: {
|
||||
name: 'bottom',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
cell: target.toString(),
|
||||
port: inputPortId.toString(),
|
||||
anchor: {
|
||||
name: 'center',
|
||||
},
|
||||
},
|
||||
label: '',
|
||||
zIndex: 1,
|
||||
}
|
||||
})
|
||||
|
||||
// 记录所有已连线的输入桩
|
||||
const inputPortConnectedMap = formattedEdges.reduce(
|
||||
(acc: { [k: string]: boolean }, edge: any) => {
|
||||
acc[edge.inputPortId] = true
|
||||
return acc
|
||||
},
|
||||
{} as { [k: string]: boolean },
|
||||
)
|
||||
|
||||
// 格式化算法组件节点
|
||||
const formattedNodes = nodes.map((nodeData: NExperimentGraph.Node) =>
|
||||
formatNodeInfoToNodeMeta(nodeData, inputPortConnectedMap),
|
||||
)
|
||||
|
||||
// 格式化群组节点
|
||||
const formattedGroups = groups.map((groupData: NExperimentGraph.Group) =>
|
||||
formatGroupInfoToNodeMeta(groupData, formattedNodes, formattedEdges),
|
||||
)
|
||||
|
||||
return {
|
||||
nodes: [
|
||||
...formattedNodes,
|
||||
...formattedGroups.filter(
|
||||
(group: any) => !!group?.data?.includedNodes?.length,
|
||||
),
|
||||
],
|
||||
edges: formattedEdges,
|
||||
}
|
||||
}
|
127
examples/x6-app-dag/src/pages/rx-models/typing.d.ts
vendored
127
examples/x6-app-dag/src/pages/rx-models/typing.d.ts
vendored
@ -1,127 +0,0 @@
|
||||
import { EdgeView } from '@antv/x6'
|
||||
|
||||
// 实验
|
||||
export namespace NExperiment {
|
||||
export interface Experiment {
|
||||
description: string
|
||||
name: string
|
||||
id: number
|
||||
gmtCreate: string
|
||||
}
|
||||
}
|
||||
|
||||
// 实验图
|
||||
export namespace NExperimentGraph {
|
||||
export interface ExperimentGraph {
|
||||
nodes: Node[]
|
||||
groups: Group[]
|
||||
links: Link[]
|
||||
}
|
||||
|
||||
export interface ModalParams {
|
||||
type: string
|
||||
experimentId: string
|
||||
nodeInstanceId?: number
|
||||
node?: Node
|
||||
ctx?: any
|
||||
}
|
||||
|
||||
export interface ContextMenuInfo {
|
||||
type: 'edge' | 'graph'
|
||||
data: EdgeView.PositionEventArgs<any>
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
outPorts: OutPort[]
|
||||
inPorts: InPort[]
|
||||
catId: number
|
||||
positionX: number
|
||||
positionY: number
|
||||
codeName: string
|
||||
category: string
|
||||
name: string
|
||||
id: string
|
||||
nodeInstanceId?: number
|
||||
groupId: number
|
||||
status: number
|
||||
}
|
||||
|
||||
export interface Port {
|
||||
sequence: number
|
||||
id: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface OutPort extends Port {}
|
||||
|
||||
export interface InPort extends Port {}
|
||||
|
||||
export interface Group {
|
||||
isCollapsed: boolean
|
||||
experimentId: number
|
||||
name: string
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
inputPortId: number
|
||||
outputPortId: number
|
||||
source: number
|
||||
type?: string
|
||||
target: number
|
||||
}
|
||||
}
|
||||
|
||||
// 执行状态
|
||||
export namespace NExecutionStatus {
|
||||
// 当前选中的组件
|
||||
export interface ActiveNode {
|
||||
type: 'legacy' | 'algo'
|
||||
[k: string]: any
|
||||
}
|
||||
|
||||
// 实验的执行状态
|
||||
export interface ExecutionStatus {
|
||||
instStatus: InstStatus
|
||||
execInfo: ExecInfo
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface InstStatus {
|
||||
[k: string]: string
|
||||
}
|
||||
|
||||
export interface GroupStatus {}
|
||||
|
||||
export interface ExecInfo {
|
||||
[k: string]: ExecDetail
|
||||
}
|
||||
|
||||
export interface ExecDetail {
|
||||
defName: string
|
||||
quickViewData: QuickViewData
|
||||
jobStatus: string
|
||||
percentage: number
|
||||
lastTime: number
|
||||
name: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
}
|
||||
|
||||
export interface QuickViewData {}
|
||||
}
|
||||
|
||||
// 模型
|
||||
export namespace NModel {
|
||||
export interface Model {
|
||||
codeName: string
|
||||
isNew: boolean
|
||||
catId: number
|
||||
parentId: number
|
||||
category: string
|
||||
owner: string
|
||||
name: string
|
||||
id: number
|
||||
type: string
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "./",
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@@/*": ["src/.umi/*"]
|
||||
},
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"mock/**/*",
|
||||
"src/**/*",
|
||||
"config/**/*",
|
||||
".umirc.ts",
|
||||
"typings.d.ts"
|
||||
]
|
||||
}
|
10
examples/x6-app-dag/typings.d.ts
vendored
10
examples/x6-app-dag/typings.d.ts
vendored
@ -1,10 +0,0 @@
|
||||
declare module '*.css'
|
||||
declare module '*.less'
|
||||
declare module '*.png'
|
||||
declare module '*.svg' {
|
||||
export function ReactComponent(
|
||||
props: React.SVGProps<SVGSVGElement>,
|
||||
): React.ReactElement
|
||||
const url: string
|
||||
export default url
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
@ -1,2 +0,0 @@
|
||||
BROWSER=none
|
||||
ESLINT=1
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "eslint-config-umi"
|
||||
}
|
19
examples/x6-app-draw/.gitignore
vendored
19
examples/x6-app-draw/.gitignore
vendored
@ -1,19 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
react-app-env.d.ts
|
||||
/npm-debug.log*
|
||||
/yarn-error.log
|
||||
/yarn.lock
|
||||
/package-lock.json
|
||||
|
||||
# production
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
# umi
|
||||
.umi
|
||||
.umi-production
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user