canvas-node.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import { apiInterceptors, refreshFlowNodeById } from '@/client/api';
  2. import { IFlowNode } from '@/types/flow';
  3. import { getUniqueNodeId, removeIndexFromNodeId } from '@/utils/flow';
  4. import { CopyOutlined, DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons';
  5. import { Form, Popover, Tooltip } from 'antd';
  6. import classNames from 'classnames';
  7. import { cloneDeep } from 'lodash';
  8. import Image from 'next/image';
  9. import { useState } from 'react';
  10. import { useReactFlow } from 'reactflow';
  11. import IconWrapper from '../common/icon-wrapper';
  12. import NodeHandler from './node-handler';
  13. import NodeParamHandler from './node-param-handler';
  14. type CanvasNodeProps = {
  15. data: IFlowNode;
  16. };
  17. function TypeLabel({ label }: { label: string }) {
  18. return <div className='w-full h-8 align-middle font-semibold'>{label}</div>;
  19. }
  20. const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
  21. const node = data;
  22. const { inputs, outputs, parameters, flow_type: flowType } = node;
  23. const [isHovered, setIsHovered] = useState(false);
  24. const reactFlow = useReactFlow();
  25. const [form] = Form.useForm();
  26. function onHover() {
  27. setIsHovered(true);
  28. }
  29. function onLeave() {
  30. setIsHovered(false);
  31. }
  32. function copyNode(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
  33. e.preventDefault();
  34. e.stopPropagation();
  35. const nodes = reactFlow.getNodes();
  36. const originalNode = nodes.find(item => item.id === node.id);
  37. if (originalNode) {
  38. const newNodeId = getUniqueNodeId(originalNode as IFlowNode, nodes);
  39. const cloneNode = cloneDeep(originalNode);
  40. const duplicatedNode = {
  41. ...cloneNode,
  42. id: newNodeId,
  43. position: {
  44. x: cloneNode.position.x + 400,
  45. y: cloneNode.position.y,
  46. },
  47. positionAbsolute: {
  48. x: cloneNode.positionAbsolute!.x + 400,
  49. y: cloneNode.positionAbsolute!.y,
  50. },
  51. data: {
  52. ...cloneNode.data,
  53. id: newNodeId,
  54. },
  55. selected: false,
  56. };
  57. reactFlow.setNodes(nodes => [...nodes, duplicatedNode]);
  58. }
  59. }
  60. function deleteNode(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
  61. e.preventDefault();
  62. e.stopPropagation();
  63. reactFlow.setNodes(nodes => nodes.filter(item => item.id !== node.id));
  64. reactFlow.setEdges(edges => edges.filter(edge => edge.source !== node.id && edge.target !== node.id));
  65. }
  66. function updateCurrentNodeValue(changedKey: string, changedVal: any) {
  67. parameters.forEach(item => {
  68. if (item.name === changedKey) {
  69. item.value = changedVal;
  70. }
  71. });
  72. }
  73. async function updateDependsNodeValue(changedKey: string, changedVal: any) {
  74. const dependParamNodes = parameters.filter(({ ui }) => ui?.refresh_depends?.includes(changedKey));
  75. if (dependParamNodes?.length === 0) return;
  76. dependParamNodes.forEach(async item => {
  77. const params = {
  78. id: removeIndexFromNodeId(data?.id),
  79. type_name: data.type_name,
  80. type_cls: data.type_cls,
  81. flow_type: 'operator' as const,
  82. refresh: [
  83. {
  84. name: item.name,
  85. depends: [
  86. {
  87. name: changedKey,
  88. value: changedVal,
  89. has_value: true,
  90. },
  91. ],
  92. },
  93. ],
  94. };
  95. const [_, res] = await apiInterceptors(refreshFlowNodeById(params));
  96. // update value of the node
  97. if (res) {
  98. reactFlow.setNodes(nodes =>
  99. nodes.map(n => {
  100. return n.id === node.id
  101. ? {
  102. ...n,
  103. data: {
  104. ...n.data,
  105. parameters: res.parameters,
  106. },
  107. }
  108. : n;
  109. }),
  110. );
  111. }
  112. });
  113. }
  114. function onParameterValuesChange(changedValues: any) {
  115. const [changedKey, changedVal] = Object.entries(changedValues)[0];
  116. updateCurrentNodeValue(changedKey, changedVal);
  117. if (changedVal) {
  118. updateDependsNodeValue(changedKey, changedVal);
  119. }
  120. }
  121. function renderOutput(data: IFlowNode) {
  122. if (flowType === 'operator' && outputs?.length > 0) {
  123. return (
  124. <div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
  125. <TypeLabel label='Outputs' />
  126. {outputs?.map((output, index) => (
  127. <NodeHandler
  128. key={`${data.id}_input_${index}`}
  129. node={data}
  130. data={output}
  131. type='source'
  132. label='outputs'
  133. index={index}
  134. />
  135. ))}
  136. </div>
  137. );
  138. } else if (flowType === 'resource') {
  139. // resource nodes show output default
  140. return (
  141. <div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
  142. <TypeLabel label='Outputs' />
  143. <NodeHandler key={`${data.id}_input_0`} node={data} data={data} type='source' label='outputs' index={0} />
  144. </div>
  145. );
  146. }
  147. }
  148. return (
  149. <Popover
  150. placement='rightTop'
  151. trigger={['hover']}
  152. content={
  153. <>
  154. <IconWrapper className='hover:text-blue-500'>
  155. <CopyOutlined className='h-full text-lg cursor-pointer' onClick={copyNode} />
  156. </IconWrapper>
  157. <IconWrapper className='mt-2 hover:text-red-500'>
  158. <DeleteOutlined className='h-full text-lg cursor-pointer' onClick={deleteNode} />
  159. </IconWrapper>
  160. <IconWrapper className='mt-2'>
  161. <Tooltip
  162. title={
  163. <>
  164. <p className='font-bold'>{node.label}</p>
  165. <p>{node.description}</p>
  166. </>
  167. }
  168. placement='right'
  169. >
  170. <InfoCircleOutlined className='h-full text-lg cursor-pointer' />
  171. </Tooltip>
  172. </IconWrapper>
  173. </>
  174. }
  175. >
  176. <div
  177. className={classNames(
  178. 'h-auto rounded-xl shadow-md px-2 py-4 border bg-white dark:bg-zinc-800 cursor-grab flex flex-col space-y-2 text-sm',
  179. {
  180. 'w-80': node?.tags?.ui_size === 'middle' || !node?.tags?.ui_size,
  181. 'w-[256px]': node?.tags?.ui_size === 'small',
  182. 'w-[530px]': node?.tags?.ui_size === 'large',
  183. 'border-blue-500': node.selected || isHovered,
  184. 'border-stone-400 dark:border-white': !node.selected && !isHovered,
  185. 'border-dashed': flowType !== 'operator',
  186. 'border-red-600': node.invalid,
  187. },
  188. )}
  189. onMouseEnter={onHover}
  190. onMouseLeave={onLeave}
  191. >
  192. {/* icon and label */}
  193. <div className='flex flex-row items-center'>
  194. <Image src={'/icons/node/vis.png'} width={24} height={24} alt='' />
  195. <p className='ml-2 text-lg font-bold text-ellipsis overflow-hidden whitespace-nowrap'>{node.label}</p>
  196. </div>
  197. {inputs?.length > 0 && (
  198. <div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
  199. <TypeLabel label='Inputs' />
  200. <div className='flex flex-col space-y-2'>
  201. {inputs?.map((item, index) => (
  202. <NodeHandler
  203. key={`${node.id}_input_${index}`}
  204. node={node}
  205. data={item}
  206. type='target'
  207. label='inputs'
  208. index={index}
  209. />
  210. ))}
  211. </div>
  212. </div>
  213. )}
  214. {parameters?.length > 0 && (
  215. <div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
  216. <TypeLabel label='Parameters' />
  217. <Form
  218. form={form}
  219. layout='vertical'
  220. onValuesChange={onParameterValuesChange}
  221. className='flex flex-col space-y-3 text-neutral-500'
  222. >
  223. {parameters?.map((item, index) => (
  224. <NodeParamHandler
  225. key={`${node.id}_param_${index}`}
  226. formValuesChange={onParameterValuesChange}
  227. node={node}
  228. paramData={item}
  229. label='parameters'
  230. index={index}
  231. />
  232. ))}
  233. </Form>
  234. </div>
  235. )}
  236. {renderOutput(node)}
  237. </div>
  238. </Popover>
  239. );
  240. };
  241. export default CanvasNode;