Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

use-workflow-interactions.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. import {
  2. useCallback,
  3. useState,
  4. } from 'react'
  5. import { useTranslation } from 'react-i18next'
  6. import { useReactFlow, useStoreApi } from 'reactflow'
  7. import produce from 'immer'
  8. import { useStore, useWorkflowStore } from '../store'
  9. import {
  10. CUSTOM_NODE, DSL_EXPORT_CHECK,
  11. NODE_LAYOUT_HORIZONTAL_PADDING,
  12. NODE_LAYOUT_VERTICAL_PADDING,
  13. WORKFLOW_DATA_UPDATE,
  14. } from '../constants'
  15. import type { Node, WorkflowDataUpdater } from '../types'
  16. import { BlockEnum, ControlMode } from '../types'
  17. import {
  18. getLayoutByDagre,
  19. getLayoutForChildNodes,
  20. initialEdges,
  21. initialNodes,
  22. } from '../utils'
  23. import {
  24. useNodesReadOnly,
  25. useSelectionInteractions,
  26. useWorkflowReadOnly,
  27. } from '../hooks'
  28. import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync'
  29. import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-without-sync'
  30. import { useNodesSyncDraft } from './use-nodes-sync-draft'
  31. import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
  32. import { useEventEmitterContextContext } from '@/context/event-emitter'
  33. import { fetchWorkflowDraft } from '@/service/workflow'
  34. import { exportAppConfig } from '@/service/apps'
  35. import { useToastContext } from '@/app/components/base/toast'
  36. import { useStore as useAppStore } from '@/app/components/app/store'
  37. export const useWorkflowInteractions = () => {
  38. const workflowStore = useWorkflowStore()
  39. const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
  40. const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
  41. const handleCancelDebugAndPreviewPanel = useCallback(() => {
  42. workflowStore.setState({
  43. showDebugAndPreviewPanel: false,
  44. workflowRunningData: undefined,
  45. })
  46. handleNodeCancelRunningStatus()
  47. handleEdgeCancelRunningStatus()
  48. }, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus])
  49. return {
  50. handleCancelDebugAndPreviewPanel,
  51. }
  52. }
  53. export const useWorkflowMoveMode = () => {
  54. const setControlMode = useStore(s => s.setControlMode)
  55. const {
  56. getNodesReadOnly,
  57. } = useNodesReadOnly()
  58. const { handleSelectionCancel } = useSelectionInteractions()
  59. const handleModePointer = useCallback(() => {
  60. if (getNodesReadOnly())
  61. return
  62. setControlMode(ControlMode.Pointer)
  63. }, [getNodesReadOnly, setControlMode])
  64. const handleModeHand = useCallback(() => {
  65. if (getNodesReadOnly())
  66. return
  67. setControlMode(ControlMode.Hand)
  68. handleSelectionCancel()
  69. }, [getNodesReadOnly, setControlMode, handleSelectionCancel])
  70. return {
  71. handleModePointer,
  72. handleModeHand,
  73. }
  74. }
  75. export const useWorkflowOrganize = () => {
  76. const workflowStore = useWorkflowStore()
  77. const store = useStoreApi()
  78. const reactflow = useReactFlow()
  79. const { getNodesReadOnly } = useNodesReadOnly()
  80. const { saveStateToHistory } = useWorkflowHistory()
  81. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  82. const handleLayout = useCallback(async () => {
  83. if (getNodesReadOnly())
  84. return
  85. workflowStore.setState({ nodeAnimation: true })
  86. const {
  87. getNodes,
  88. edges,
  89. setNodes,
  90. } = store.getState()
  91. const { setViewport } = reactflow
  92. const nodes = getNodes()
  93. const loopAndIterationNodes = nodes.filter(
  94. node => (node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
  95. && !node.parentId
  96. && node.type === CUSTOM_NODE,
  97. )
  98. const childLayoutsMap: Record<string, any> = {}
  99. loopAndIterationNodes.forEach((node) => {
  100. childLayoutsMap[node.id] = getLayoutForChildNodes(node.id, nodes, edges)
  101. })
  102. const containerSizeChanges: Record<string, { width: number, height: number }> = {}
  103. loopAndIterationNodes.forEach((parentNode) => {
  104. const childLayout = childLayoutsMap[parentNode.id]
  105. if (!childLayout) return
  106. let minX = Infinity
  107. let minY = Infinity
  108. let maxX = -Infinity
  109. let maxY = -Infinity
  110. let hasChildren = false
  111. const childNodes = nodes.filter(node => node.parentId === parentNode.id)
  112. childNodes.forEach((node) => {
  113. if (childLayout.node(node.id)) {
  114. hasChildren = true
  115. const childNodeWithPosition = childLayout.node(node.id)
  116. const nodeX = childNodeWithPosition.x - node.width! / 2
  117. const nodeY = childNodeWithPosition.y - node.height! / 2
  118. minX = Math.min(minX, nodeX)
  119. minY = Math.min(minY, nodeY)
  120. maxX = Math.max(maxX, nodeX + node.width!)
  121. maxY = Math.max(maxY, nodeY + node.height!)
  122. }
  123. })
  124. if (hasChildren) {
  125. const requiredWidth = maxX - minX + NODE_LAYOUT_HORIZONTAL_PADDING * 2
  126. const requiredHeight = maxY - minY + NODE_LAYOUT_VERTICAL_PADDING * 2
  127. containerSizeChanges[parentNode.id] = {
  128. width: Math.max(parentNode.width || 0, requiredWidth),
  129. height: Math.max(parentNode.height || 0, requiredHeight),
  130. }
  131. }
  132. })
  133. const nodesWithUpdatedSizes = produce(nodes, (draft) => {
  134. draft.forEach((node) => {
  135. if ((node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
  136. && containerSizeChanges[node.id]) {
  137. node.width = containerSizeChanges[node.id].width
  138. node.height = containerSizeChanges[node.id].height
  139. if (node.data.type === BlockEnum.Loop) {
  140. node.data.width = containerSizeChanges[node.id].width
  141. node.data.height = containerSizeChanges[node.id].height
  142. }
  143. else if (node.data.type === BlockEnum.Iteration) {
  144. node.data.width = containerSizeChanges[node.id].width
  145. node.data.height = containerSizeChanges[node.id].height
  146. }
  147. }
  148. })
  149. })
  150. const layout = getLayoutByDagre(nodesWithUpdatedSizes, edges)
  151. const rankMap = {} as Record<string, Node>
  152. nodesWithUpdatedSizes.forEach((node) => {
  153. if (!node.parentId && node.type === CUSTOM_NODE) {
  154. const rank = layout.node(node.id).rank!
  155. if (!rankMap[rank]) {
  156. rankMap[rank] = node
  157. }
  158. else {
  159. if (rankMap[rank].position.y > node.position.y)
  160. rankMap[rank] = node
  161. }
  162. }
  163. })
  164. const newNodes = produce(nodesWithUpdatedSizes, (draft) => {
  165. draft.forEach((node) => {
  166. if (!node.parentId && node.type === CUSTOM_NODE) {
  167. const nodeWithPosition = layout.node(node.id)
  168. node.position = {
  169. x: nodeWithPosition.x - node.width! / 2,
  170. y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
  171. }
  172. }
  173. })
  174. loopAndIterationNodes.forEach((parentNode) => {
  175. const childLayout = childLayoutsMap[parentNode.id]
  176. if (!childLayout) return
  177. const childNodes = draft.filter(node => node.parentId === parentNode.id)
  178. let minX = Infinity
  179. let minY = Infinity
  180. childNodes.forEach((node) => {
  181. if (childLayout.node(node.id)) {
  182. const childNodeWithPosition = childLayout.node(node.id)
  183. const nodeX = childNodeWithPosition.x - node.width! / 2
  184. const nodeY = childNodeWithPosition.y - node.height! / 2
  185. minX = Math.min(minX, nodeX)
  186. minY = Math.min(minY, nodeY)
  187. }
  188. })
  189. childNodes.forEach((node) => {
  190. if (childLayout.node(node.id)) {
  191. const childNodeWithPosition = childLayout.node(node.id)
  192. node.position = {
  193. x: NODE_LAYOUT_HORIZONTAL_PADDING + (childNodeWithPosition.x - node.width! / 2 - minX),
  194. y: NODE_LAYOUT_VERTICAL_PADDING + (childNodeWithPosition.y - node.height! / 2 - minY),
  195. }
  196. }
  197. })
  198. })
  199. })
  200. setNodes(newNodes)
  201. const zoom = 0.7
  202. setViewport({
  203. x: 0,
  204. y: 0,
  205. zoom,
  206. })
  207. saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize)
  208. setTimeout(() => {
  209. handleSyncWorkflowDraft()
  210. })
  211. }, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
  212. return {
  213. handleLayout,
  214. }
  215. }
  216. export const useWorkflowZoom = () => {
  217. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  218. const { getWorkflowReadOnly } = useWorkflowReadOnly()
  219. const {
  220. zoomIn,
  221. zoomOut,
  222. zoomTo,
  223. fitView,
  224. } = useReactFlow()
  225. const handleFitView = useCallback(() => {
  226. if (getWorkflowReadOnly())
  227. return
  228. fitView()
  229. handleSyncWorkflowDraft()
  230. }, [getWorkflowReadOnly, fitView, handleSyncWorkflowDraft])
  231. const handleBackToOriginalSize = useCallback(() => {
  232. if (getWorkflowReadOnly())
  233. return
  234. zoomTo(1)
  235. handleSyncWorkflowDraft()
  236. }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
  237. const handleSizeToHalf = useCallback(() => {
  238. if (getWorkflowReadOnly())
  239. return
  240. zoomTo(0.5)
  241. handleSyncWorkflowDraft()
  242. }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
  243. const handleZoomOut = useCallback(() => {
  244. if (getWorkflowReadOnly())
  245. return
  246. zoomOut()
  247. handleSyncWorkflowDraft()
  248. }, [getWorkflowReadOnly, zoomOut, handleSyncWorkflowDraft])
  249. const handleZoomIn = useCallback(() => {
  250. if (getWorkflowReadOnly())
  251. return
  252. zoomIn()
  253. handleSyncWorkflowDraft()
  254. }, [getWorkflowReadOnly, zoomIn, handleSyncWorkflowDraft])
  255. return {
  256. handleFitView,
  257. handleBackToOriginalSize,
  258. handleSizeToHalf,
  259. handleZoomOut,
  260. handleZoomIn,
  261. }
  262. }
  263. export const useWorkflowUpdate = () => {
  264. const reactflow = useReactFlow()
  265. const { eventEmitter } = useEventEmitterContextContext()
  266. const handleUpdateWorkflowCanvas = useCallback((payload: WorkflowDataUpdater) => {
  267. const {
  268. nodes,
  269. edges,
  270. viewport,
  271. } = payload
  272. const { setViewport } = reactflow
  273. eventEmitter?.emit({
  274. type: WORKFLOW_DATA_UPDATE,
  275. payload: {
  276. nodes: initialNodes(nodes, edges),
  277. edges: initialEdges(edges, nodes),
  278. },
  279. } as any)
  280. setViewport(viewport)
  281. }, [eventEmitter, reactflow])
  282. return {
  283. handleUpdateWorkflowCanvas,
  284. }
  285. }
  286. export const useDSL = () => {
  287. const { t } = useTranslation()
  288. const { notify } = useToastContext()
  289. const { eventEmitter } = useEventEmitterContextContext()
  290. const [exporting, setExporting] = useState(false)
  291. const { doSyncWorkflowDraft } = useNodesSyncDraft()
  292. const appDetail = useAppStore(s => s.appDetail)
  293. const handleExportDSL = useCallback(async (include = false) => {
  294. if (!appDetail)
  295. return
  296. if (exporting)
  297. return
  298. try {
  299. setExporting(true)
  300. await doSyncWorkflowDraft()
  301. const { data } = await exportAppConfig({
  302. appID: appDetail.id,
  303. include,
  304. })
  305. const a = document.createElement('a')
  306. const file = new Blob([data], { type: 'application/yaml' })
  307. a.href = URL.createObjectURL(file)
  308. a.download = `${appDetail.name}.yml`
  309. a.click()
  310. }
  311. catch {
  312. notify({ type: 'error', message: t('app.exportFailed') })
  313. }
  314. finally {
  315. setExporting(false)
  316. }
  317. }, [appDetail, notify, t, doSyncWorkflowDraft, exporting])
  318. const exportCheck = useCallback(async () => {
  319. if (!appDetail)
  320. return
  321. try {
  322. const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail?.id}/workflows/draft`)
  323. const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
  324. if (list.length === 0) {
  325. handleExportDSL()
  326. return
  327. }
  328. eventEmitter?.emit({
  329. type: DSL_EXPORT_CHECK,
  330. payload: {
  331. data: list,
  332. },
  333. } as any)
  334. }
  335. catch {
  336. notify({ type: 'error', message: t('app.exportFailed') })
  337. }
  338. }, [appDetail, eventEmitter, handleExportDSL, notify, t])
  339. return {
  340. exportCheck,
  341. handleExportDSL,
  342. }
  343. }
  344. export const useWorkflowCanvasMaximize = () => {
  345. const { eventEmitter } = useEventEmitterContextContext()
  346. const maximizeCanvas = useStore(s => s.maximizeCanvas)
  347. const setMaximizeCanvas = useStore(s => s.setMaximizeCanvas)
  348. const {
  349. getNodesReadOnly,
  350. } = useNodesReadOnly()
  351. const handleToggleMaximizeCanvas = useCallback(() => {
  352. if (getNodesReadOnly())
  353. return
  354. setMaximizeCanvas(!maximizeCanvas)
  355. localStorage.setItem('workflow-canvas-maximize', String(!maximizeCanvas))
  356. eventEmitter?.emit({
  357. type: 'workflow-canvas-maximize',
  358. payload: !maximizeCanvas,
  359. } as any)
  360. }, [eventEmitter, getNodesReadOnly, maximizeCanvas, setMaximizeCanvas])
  361. return {
  362. handleToggleMaximizeCanvas,
  363. }
  364. }