add-nodes.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { apiInterceptors, getFlowNodes } from '@/client/api';
  2. import { IFlowNode } from '@/types/flow';
  3. import { FLOW_NODES_KEY } from '@/utils';
  4. import { PlusOutlined } from '@ant-design/icons';
  5. import { Badge, Button, Collapse, CollapseProps, Input, Popover } from 'antd';
  6. import React, { useEffect, useMemo, useState } from 'react';
  7. import { useTranslation } from 'react-i18next';
  8. import StaticNodes from './static-nodes';
  9. const { Search } = Input;
  10. type GroupType = { category: string; categoryLabel: string; nodes: IFlowNode[] };
  11. const AddNodes: React.FC = () => {
  12. const { t } = useTranslation();
  13. const [operators, setOperators] = useState<Array<IFlowNode>>([]);
  14. const [resources, setResources] = useState<Array<IFlowNode>>([]);
  15. const [operatorsGroup, setOperatorsGroup] = useState<GroupType[]>([]);
  16. const [resourcesGroup, setResourcesGroup] = useState<GroupType[]>([]);
  17. const [searchValue, setSearchValue] = useState<string>('');
  18. useEffect(() => {
  19. getNodes();
  20. }, []);
  21. async function getNodes() {
  22. const [_, data] = await apiInterceptors(getFlowNodes());
  23. if (data && data.length > 0) {
  24. localStorage.setItem(FLOW_NODES_KEY, JSON.stringify(data));
  25. const operatorNodes = data.filter(node => node.flow_type === 'operator');
  26. const resourceNodes = data.filter(node => node.flow_type === 'resource');
  27. setOperators(operatorNodes);
  28. setResources(resourceNodes);
  29. setOperatorsGroup(groupNodes(operatorNodes));
  30. setResourcesGroup(groupNodes(resourceNodes));
  31. }
  32. }
  33. function groupNodes(data: IFlowNode[]) {
  34. const groups: GroupType[] = [];
  35. const categoryMap: Record<string, { category: string; categoryLabel: string; nodes: IFlowNode[] }> = {};
  36. data.forEach(item => {
  37. const { category, category_label } = item;
  38. if (!categoryMap[category]) {
  39. categoryMap[category] = { category, categoryLabel: category_label, nodes: [] };
  40. groups.push(categoryMap[category]);
  41. }
  42. categoryMap[category].nodes.push(item);
  43. });
  44. return groups;
  45. }
  46. const operatorItems: CollapseProps['items'] = useMemo(() => {
  47. if (!searchValue) {
  48. return operatorsGroup.map(({ category, categoryLabel, nodes }) => ({
  49. key: category,
  50. label: categoryLabel,
  51. children: <StaticNodes nodes={nodes} />,
  52. extra: (
  53. <Badge
  54. showZero
  55. count={nodes.length || 0}
  56. style={{ backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474' }}
  57. />
  58. ),
  59. }));
  60. } else {
  61. const searchedNodes = operators.filter(node => node.label.toLowerCase().includes(searchValue.toLowerCase()));
  62. return groupNodes(searchedNodes).map(({ category, categoryLabel, nodes }) => ({
  63. key: category,
  64. label: categoryLabel,
  65. children: <StaticNodes nodes={nodes} />,
  66. extra: (
  67. <Badge
  68. showZero
  69. count={nodes.length || 0}
  70. style={{ backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474' }}
  71. />
  72. ),
  73. }));
  74. }
  75. }, [operatorsGroup, searchValue]);
  76. const resourceItems: CollapseProps['items'] = useMemo(() => {
  77. if (!searchValue) {
  78. return resourcesGroup.map(({ category, categoryLabel, nodes }) => ({
  79. key: category,
  80. label: categoryLabel,
  81. children: <StaticNodes nodes={nodes} />,
  82. extra: (
  83. <Badge
  84. showZero
  85. count={nodes.length || 0}
  86. style={{ backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474' }}
  87. />
  88. ),
  89. }));
  90. } else {
  91. const searchedNodes = resources.filter(node => node.label.toLowerCase().includes(searchValue.toLowerCase()));
  92. return groupNodes(searchedNodes).map(({ category, categoryLabel, nodes }) => ({
  93. key: category,
  94. label: categoryLabel,
  95. children: <StaticNodes nodes={nodes} />,
  96. extra: (
  97. <Badge
  98. showZero
  99. count={nodes.length || 0}
  100. style={{ backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474' }}
  101. />
  102. ),
  103. }));
  104. }
  105. }, [resourcesGroup, searchValue]);
  106. function searchNode(val: string) {
  107. setSearchValue(val);
  108. }
  109. return (
  110. <Popover
  111. placement='bottom'
  112. trigger={['click']}
  113. content={
  114. <div className='w-[320px] overflow-hidden overflow-y-auto scrollbar-default'>
  115. <p className='my-2 font-bold'>{t('add_node')}</p>
  116. <Search placeholder='Search node' onSearch={searchNode} />
  117. <h2 className='my-2 ml-2 font-semibold'>{t('operators')}</h2>
  118. <Collapse
  119. className='max-h-[300px] overflow-hidden overflow-y-auto scrollbar-default'
  120. size='small'
  121. defaultActiveKey={['']}
  122. items={operatorItems}
  123. />
  124. <h2 className='my-2 ml-2 font-semibold'>{t('resource')}</h2>
  125. <Collapse
  126. className='max-h-[300px] overflow-hidden overflow-y-auto scrollbar-default'
  127. size='small'
  128. defaultActiveKey={['']}
  129. items={resourceItems}
  130. />
  131. </div>
  132. }
  133. >
  134. <Button
  135. type='primary'
  136. className='flex items-center justify-center rounded-full left-4 top-4'
  137. style={{ zIndex: 1050 }}
  138. icon={<PlusOutlined />}
  139. />
  140. </Popover>
  141. );
  142. };
  143. export default AddNodes;