Skip to content

Commit

Permalink
feat: add HTML node, g node, react node (#5654)
Browse files Browse the repository at this point in the history
* 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
Aarebecca authored Apr 18, 2024
1 parent b8c2f60 commit 79c18aa
Show file tree
Hide file tree
Showing 37 changed files with 967 additions and 29 deletions.
2 changes: 1 addition & 1 deletion packages/g6-extension-3d/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"devDependencies": {
"@antv/g": "^5.18.25",
"@antv/g-canvas": "^1.11.27",
"@antv/g6": "^5.0.0-beta.30"
"@antv/g6": "workspace:*"
},
"peerDependencies": {
"@antv/g": "^5.18.25",
Expand Down
112 changes: 112 additions & 0 deletions packages/g6-extension-react/__tests__/demos/g-node.tsx
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'],
}}
/>
);
};
24 changes: 24 additions & 0 deletions packages/g6-extension-react/__tests__/demos/graph.tsx
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');
}}
/>
);
};
3 changes: 3 additions & 0 deletions packages/g6-extension-react/__tests__/demos/index.tsx
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 packages/g6-extension-react/__tests__/demos/react-node.tsx
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>
);
};
17 changes: 17 additions & 0 deletions packages/g6-extension-react/__tests__/index.html
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>
47 changes: 47 additions & 0 deletions packages/g6-extension-react/__tests__/main.tsx
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>,
);
5 changes: 5 additions & 0 deletions packages/g6-extension-react/__tests__/unit/default.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('default', () => {
it('expect', () => {
expect(1).toBe(1);
});
});
8 changes: 8 additions & 0 deletions packages/g6-extension-react/jest.config.js
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'],
};
Loading

0 comments on commit 79c18aa

Please sign in to comment.