-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add HTML node, g node, react node (#5654)
* feat(elements): add html node * fix: fix issue that unexpected invoke sequence cause exception * refactor: adjust exports * feat(react): create g6-extension-react * test: update test case * chore: update dependencies * fix: fix cr issue * chore: adjust jest config
- Loading branch information
Showing
37 changed files
with
967 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import type { NodeData } from '@antv/g6'; | ||
import { ExtensionCategory, register } from '@antv/g6'; | ||
import { GNode, Group, Image, Rect, Text } from '../../src'; | ||
import { Graph } from '../../src/graph'; | ||
|
||
register(ExtensionCategory.NODE, 'g', GNode); | ||
|
||
type Datum = { | ||
name: string; | ||
type: 'module' | 'process'; | ||
status: 'success' | 'error'; | ||
success: number; | ||
time: number; | ||
failure: number; | ||
}; | ||
|
||
const Node = ({ data, size }: { data: NodeData; size: [number, number] }) => { | ||
const [width, height] = size; | ||
|
||
const { name, type, status, success, time, failure } = data.data as Datum; | ||
const color = status === 'success' ? '#30BF78' : '#F4664A'; | ||
const radius = 4; | ||
|
||
const titleMap = { | ||
success: 'Success', | ||
time: 'Time', | ||
failure: 'Failure', | ||
}; | ||
|
||
const format = (cat: string, value: number) => { | ||
if (cat === 'success') return `${value}%`; | ||
if (cat === 'time') return `${value}min`; | ||
return value.toString(); | ||
}; | ||
|
||
const highlight = (cat: string, value: number) => { | ||
if (cat === 'success') { | ||
if (value >= 90) return 'green'; | ||
if (value < 60) return 'red'; | ||
return 'gray'; | ||
} | ||
if (cat === 'time') { | ||
if (value <= 10) return 'green'; | ||
if (value >= 30) return 'red'; | ||
return 'gray'; | ||
} | ||
if (value >= 20) return 'red'; | ||
if (value >= 5) return 'orange'; | ||
return 'gray'; | ||
}; | ||
|
||
return ( | ||
<Group> | ||
<Rect width={width} height={height} stroke={color} fill={'white'} radius={radius}> | ||
<Rect width={width} height={20} fill={color} radius={[radius, radius, 0, 0]}> | ||
<Image | ||
src={ | ||
type === 'module' | ||
? 'https://gw.alipayobjects.com/mdn/rms_8fd2eb/afts/img/A*0HC-SawWYUoAAAAAAAAAAABkARQnAQ' | ||
: 'https://gw.alipayobjects.com/mdn/rms_8fd2eb/afts/img/A*sxK0RJ1UhNkAAAAAAAAAAABkARQnAQ' | ||
} | ||
x={2} | ||
y={2} | ||
width={16} | ||
height={16} | ||
/> | ||
<Text text={name} textBaseline="top" fill="#fff" fontSize={14} dx={20} dy={2} /> | ||
</Rect> | ||
<Group transform="translate(5,40)"> | ||
{Object.entries({ success, time, failure }).map(([key, value], index) => ( | ||
<Group key={index} transform={`translate(${(index * width) / 3}, 0)`}> | ||
<Text text={titleMap[key as keyof typeof titleMap]} fontSize={12} fill="gray" /> | ||
<Text text={format(key, value)} fontSize={12} dy={16} fill={highlight(key, value)} /> | ||
</Group> | ||
))} | ||
</Group> | ||
</Rect> | ||
</Group> | ||
); | ||
}; | ||
|
||
export const GNodeDemo = () => { | ||
return ( | ||
<Graph | ||
options={{ | ||
data: { | ||
nodes: [ | ||
{ | ||
id: 'node-1', | ||
data: { name: 'Module', type: 'module', status: 'success', success: 90, time: 58, failure: 8 }, | ||
style: { x: 100, y: 100 }, | ||
}, | ||
{ | ||
id: 'node-2', | ||
data: { name: 'Process', type: 'process', status: 'error', success: 11, time: 12, failure: 26 }, | ||
style: { x: 300, y: 100 }, | ||
}, | ||
], | ||
edges: [{ source: 'node-1', target: 'node-2' }], | ||
}, | ||
node: { | ||
style: { | ||
type: 'g', | ||
size: [180, 60], // tell G6 the size of the node | ||
component: (data: NodeData) => <Node data={data} size={[180, 60]} />, | ||
}, | ||
}, | ||
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'], | ||
}} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Graph } from '../../src/graph'; | ||
|
||
export const G6Graph = () => { | ||
return ( | ||
<Graph | ||
options={{ | ||
data: { | ||
nodes: [ | ||
{ id: 'node-1', style: { x: 100, y: 100, labelText: 'Hello' } }, | ||
{ id: 'node-2', style: { x: 300, y: 100, labelText: 'World' } }, | ||
], | ||
edges: [{ source: 'node-1', target: 'node-2' }], | ||
}, | ||
behaviors: ['drag-element'], | ||
}} | ||
onRender={() => { | ||
console.log('render'); | ||
}} | ||
onDestroy={() => { | ||
console.log('destroy'); | ||
}} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './g-node'; | ||
export * from './graph'; | ||
export * from './react-node'; |
166 changes: 166 additions & 0 deletions
166
packages/g6-extension-react/__tests__/demos/react-node.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import { DatabaseFilled } from '@ant-design/icons'; | ||
import type { Graph as G6Graph, G6Spec, NodeData } from '@antv/g6'; | ||
import { ExtensionCategory, register } from '@antv/g6'; | ||
import { Badge, Button, Flex, Form, Input, Layout, Select, Table, Tag, Typography } from 'antd'; | ||
import { useRef, useState } from 'react'; | ||
import { ReactNode } from '../../src'; | ||
import { Graph } from '../../src/graph'; | ||
|
||
const { Content, Footer } = Layout; | ||
const { Text } = Typography; | ||
|
||
register(ExtensionCategory.NODE, 'react', ReactNode); | ||
|
||
type Datum = { | ||
name: string; | ||
status: 'success' | 'error' | 'warning'; | ||
type: 'local' | 'remote'; | ||
url: string; | ||
}; | ||
|
||
const Node = ({ data, onChange }: { data: NodeData; onChange?: (value: string) => void }) => { | ||
const { status, type } = data.data as Datum; | ||
|
||
return ( | ||
<Flex style={{ width: '100%', height: '100%', background: '#fff', padding: 10, borderRadius: 5 }} vertical> | ||
<Flex align="center" justify="space-between"> | ||
<Text> | ||
<DatabaseFilled /> | ||
Server | ||
<Tag>{type}</Tag> | ||
</Text> | ||
<Badge status={status} /> | ||
</Flex> | ||
<Text type="secondary">{data.id}</Text> | ||
<Flex align="center"> | ||
<Text style={{ flexShrink: 0 }}> | ||
<Text type="danger">*</Text>URL: | ||
</Text> | ||
<Input | ||
style={{ borderRadius: 0, borderBottom: '1px solid #d9d9d9' }} | ||
variant="borderless" | ||
value={data.data?.url as string} | ||
onChange={(event) => { | ||
const url = event.target.value; | ||
onChange?.(url); | ||
}} | ||
/> | ||
</Flex> | ||
</Flex> | ||
); | ||
}; | ||
|
||
export const ReactNodeDemo = () => { | ||
const graphRef = useRef<G6Graph | null>(null); | ||
|
||
const [form] = Form.useForm(); | ||
const isValidUrl = (url: string) => { | ||
return /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/.test( | ||
url, | ||
); | ||
}; | ||
|
||
const [options, setOptions] = useState<G6Spec>({ | ||
data: { | ||
nodes: [ | ||
{ | ||
id: 'local-server-1', | ||
data: { status: 'success', type: 'local', url: 'http://localhost:3000' }, | ||
style: { x: 50, y: 50 }, | ||
}, | ||
{ | ||
id: 'remote-server-1', | ||
data: { status: 'warning', type: 'remote' }, | ||
style: { x: 350, y: 50 }, | ||
}, | ||
], | ||
}, | ||
node: { | ||
style: { | ||
type: 'react', | ||
size: [240, 100], | ||
component: (data: NodeData) => ( | ||
<Node | ||
data={data} | ||
onChange={(url) => { | ||
setOptions((prev) => { | ||
if (!graphRef.current || graphRef.current.destroyed) return prev; | ||
const nodes = graphRef.current.getNodeData(); | ||
const index = nodes.findIndex((node) => node.id === data.id); | ||
const node = nodes[index]; | ||
const datum = { | ||
...node.data, | ||
url, | ||
status: url === '' ? 'warning' : isValidUrl(url) ? 'success' : 'error', | ||
} as Datum; | ||
nodes[index] = { ...node, data: datum }; | ||
return { ...prev, data: { ...prev.data, nodes } }; | ||
}); | ||
}} | ||
/> | ||
), | ||
}, | ||
}, | ||
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'], | ||
}); | ||
|
||
return ( | ||
<Layout style={{ width: 800, height: 400 }}> | ||
<Content style={{ minHeight: 400 }}> | ||
<Graph options={options} onRender={(graph) => (graphRef.current = graph)} /> | ||
</Content> | ||
<Footer> | ||
<Form form={form} initialValues={{ serverType: 'local' }}> | ||
<Form.Item label="Server Type" name="serverType"> | ||
<Select | ||
options={[ | ||
{ label: 'Local', value: 'local' }, | ||
{ label: 'Remote', value: 'remote' }, | ||
]} | ||
/> | ||
</Form.Item> | ||
<Form.Item> | ||
<Button | ||
style={{ width: '100%' }} | ||
type="primary" | ||
onClick={() => { | ||
if (!graphRef.current || graphRef.current.destroyed) return; | ||
const type = form.getFieldValue('serverType'); | ||
const status = 'warning'; | ||
const length = (options.data?.nodes || []).filter((node) => node?.data?.type === type).length; | ||
setOptions((prev) => ({ | ||
...prev, | ||
data: { | ||
...prev.data, | ||
nodes: [ | ||
...graphRef.current!.getNodeData(), | ||
{ | ||
id: `${type}-server-${length + 1}`, | ||
data: { type, status }, | ||
style: { x: type === 'local' ? 50 : 350, y: 50 + length * 120 }, | ||
}, | ||
], | ||
}, | ||
})); | ||
}} | ||
> | ||
Add Node | ||
</Button> | ||
</Form.Item> | ||
</Form> | ||
|
||
<Table | ||
columns={[ | ||
{ title: 'Server', key: 'server', dataIndex: 'server' }, | ||
{ title: 'URL', key: 'url', dataIndex: 'url' }, | ||
]} | ||
dataSource={(options.data?.nodes || []).map((node) => ({ | ||
key: node.id, | ||
server: node.id, | ||
url: (node?.data as Datum).url || 'Not Configured', | ||
}))} | ||
></Table> | ||
</Footer> | ||
</Layout> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>@antv/g6-extension-react</title> | ||
<style> | ||
body { | ||
margin: 0; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="./main.tsx"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { Alert, Flex, Select } from 'antd'; | ||
import React from 'react'; | ||
import { createRoot } from 'react-dom/client'; | ||
import { Outlet, RouterProvider, createBrowserRouter, useMatch, useNavigate } from 'react-router-dom'; | ||
import * as demos from './demos'; | ||
|
||
const App = () => { | ||
const navigate = useNavigate(); | ||
const match = useMatch('/*'); | ||
|
||
return ( | ||
<Flex vertical> | ||
<Select | ||
value={match?.params['*'] || Object.keys(demos)[0]} | ||
options={Object.keys(demos).map((label) => ({ label, value: label }))} | ||
style={{ width: 100 }} | ||
onChange={(value) => navigate(value)} | ||
/> | ||
<Outlet /> | ||
</Flex> | ||
); | ||
}; | ||
|
||
const NotFount = () => { | ||
return <Alert message="Demo Not Found" type="error" />; | ||
}; | ||
|
||
const router = createBrowserRouter([ | ||
{ | ||
path: '/', | ||
element: <App />, | ||
errorElement: <NotFount />, | ||
children: Object.entries(demos).map(([key, Demo]) => ({ | ||
path: key, | ||
element: <Demo />, | ||
})), | ||
}, | ||
]); | ||
|
||
const container = document.getElementById('root')!; | ||
const root = createRoot(container); | ||
|
||
root.render( | ||
<React.StrictMode> | ||
<RouterProvider router={router} /> | ||
</React.StrictMode>, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
describe('default', () => { | ||
it('expect', () => { | ||
expect(1).toBe(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module.exports = { | ||
transform: { | ||
'^.+\\.[tj]s$': ['@swc/jest'], | ||
}, | ||
testRegex: '(/__tests__/.*\\.(test|spec))\\.(ts|tsx|js)$', | ||
collectCoverageFrom: ['src/**/*.ts'], | ||
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], | ||
}; |
Oops, something went wrong.