antd#Tree TypeScript Examples

The following examples show how to use antd#Tree. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: DraggableTree.tsx    From PRSS with GNU General Public License v3.0 6 votes vote down vote up
render() {
    const {
      onCheck = noop,
      onSelect = noop,
      checkable,
      checkedKeys = [],
      checkStrictly,
    } = this.props;

    return (
      <div className="draggable-tree">
        <Tree
          className="draggable-tree"
          defaultExpandAll
          draggable={this.props.draggable ?? true}
          blockNode
          onDragEnter={this.onDragEnter}
          onDrop={this.onDrop}
          treeData={this.state.gData}
          onSelect={onSelect}
          onCheck={(chk: any) => {
            const checked = checkStrictly ? chk.checked : chk;
            onCheck(checked);
          }}
          showLine
          checkable={checkable}
          checkedKeys={checkedKeys}
          selectedKeys={[]}
          checkStrictly={checkStrictly}
        />
      </div>
    );
  }
Example #2
Source File: EntityHierachyTree.tsx    From jmix-frontend with Apache License 2.0 5 votes vote down vote up
EntityHierarchyTree = ({
  hierarchyProperty, items, ...rest} : EntityHierarchyTreeProps
) => {
  return (
    <Tree {...rest} treeData={toTreeData(items, hierarchyProperty)} />
  )
}
Example #3
Source File: panel-body.tsx    From XFlow with MIT License 5 votes vote down vote up
{ DirectoryTree, TreeNode } = Tree
Example #4
Source File: RoutesView.spec.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
describe("RoutesView", () => {
  it("should work", async () => {
    const onRouteSelect = jest.fn();
    const wrapper = mount(<RoutesView handleRouteSelect={onRouteSelect} />);
    await act(async () => {
      await (global as any).flushPromises();
    });
    expect(wrapper.find(Tree).length).toBe(1);
    wrapper.find(Tree).invoke("onSelect")([], {
      node: {
        props: {
          id: "R-03",
        },
      },
    } as any);
    expect(onRouteSelect).toBeCalled();
    await act(async () => {
      wrapper.find(SearchComponent).invoke("onSearch")("detail");
      await (global as any).flushPromises();
    });
    wrapper.update();
    expect(wrapper.find(".matchedStr").length).toBe(2);
    expect(wrapper.find(".ant-tree-list .ant-tree-treenode").length).toBe(2);

    await act(async () => {
      wrapper.find(SearchComponent).invoke("onSearch")("/c");
      await (global as any).flushPromises();
    });
    wrapper.update();
    expect(wrapper.find(".matchedStr").length).toBe(0);
    expect(wrapper.find(".ant-tree-list .ant-tree-treenode").length).toBe(2);

    await act(async () => {
      wrapper.find(SearchComponent).invoke("onSearch")(" ");
      await (global as any).flushPromises();
    });
    wrapper.update();
    expect(wrapper.find(".matchedStr").length).toBe(0);
  });
});
Example #5
Source File: SearchableTree.spec.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
describe("SearchableTree", () => {
  it("should work", async () => {
    const handleSelect = jest.fn();
    const handleQChange = jest.fn();
    const wrapper = mount(
      <SearchableTree
        list={list}
        defaultSelectedKeys={["R-01"]}
        icon={<BranchesOutlined />}
        field="alias"
        onSelect={handleSelect}
        onQChange={handleQChange}
      />
    );
    await act(async () => {
      await (global as any).flushPromises();
    });
    expect(wrapper.find(Tree).length).toBe(1);
    wrapper.find(Tree).invoke("onSelect")([], {
      node: {
        props: {
          id: "R-03",
        },
      },
    } as any);
    expect(handleSelect).toBeCalled();
    await act(async () => {
      wrapper.find(SearchComponent).invoke("onSearch")("detail");
      await (global as any).flushPromises();
    });
    wrapper.update();
    expect(wrapper.find(".matchedStr").length).toBe(2);
    await act(async () => {
      wrapper.find(SearchComponent).invoke("onSearch")(" ");
      await (global as any).flushPromises();
    });
    wrapper.update();
    expect(wrapper.find(".matchedStr").length).toBe(0);
  });
});
Example #6
Source File: SearchableTree.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
export function SearchableTree({
  list,
  defaultSelectedKeys,
  icon,
  field = "title",
  searchPlaceholder,
  onSelect,
  onQChange,
  customClassName,
}: SearchableTreeProps): React.ReactElement {
  const [q, setQ] = useState<string>("");
  const [selectedKeys, setSelectedKeys] = useState(defaultSelectedKeys);

  const handleSearch = (value: string): void => {
    setQ(value);
  };

  useEffect(() => {
    onQChange?.(q);
  }, [q]);

  const handleClick = (selectedKeys: React.Key[], value: any): void => {
    const selectedProps = value.node.props;
    const select = selectedProps.id ? [selectedProps.id] : [];
    setSelectedKeys(select);
    onSelect?.(selectedProps);
  };

  const titleRender = (nodeData: Record<string, any>): React.ReactElement => {
    const titleText = get(nodeData, field) as string;
    let title = <span>{titleText}</span>;
    if (q) {
      const trimQ = q.trim();
      const index = titleText.toLowerCase().indexOf(trimQ.toLowerCase());
      if (index !== -1) {
        const [beforeStr, matchStr, afterStr] = [
          titleText.substr(0, index),
          titleText.substr(index, trimQ.length),
          titleText.substr(index + trimQ.length),
        ];
        title = (
          <span>
            {beforeStr}
            {!!matchStr && (
              <span className={styles.matchedStr}>{matchStr}</span>
            )}
            {afterStr}
          </span>
        );
      }
    }
    return <span title={nodeData.path}>{title}</span>;
  };

  return (
    <div className={classNames(styles.container, customClassName)}>
      <SearchComponent
        placeholder={searchPlaceholder}
        onSearch={handleSearch}
      />
      {list?.length > 0 && (
        <div className={styles.treeWrapper}>
          <Tree
            showIcon
            defaultExpandAll={true}
            treeData={list as any}
            selectedKeys={selectedKeys}
            switcherIcon={<DownOutlined />}
            onSelect={handleClick}
            titleRender={titleRender}
            icon={icon}
            blockNode={true}
          ></Tree>
        </div>
      )}
    </div>
  );
}
Example #7
Source File: ServerManager.tsx    From anew-server with MIT License 5 votes vote down vote up
{ DirectoryTree } = Tree
Example #8
Source File: TreeFilter.tsx    From datart with Apache License 2.0 5 votes vote down vote up
TreeFilter: FC<PresentControllerFilterProps> = memo(
  ({ condition, onConditionChange }) => {
    const getSelectedKeysFromTreeModel = (list, collector: string[]) => {
      list?.map(l => {
        if (l.isSelected) {
          collector.push(l.key);
        }
        if (l.children) {
          getSelectedKeysFromTreeModel(l.children, collector);
        }
      });
    };
    const [treeData] = useState(() => {
      if (Array.isArray(condition?.value)) {
        return condition?.value;
      }
      return [];
    });
    const [selectedKeys, setSelectedKeys] = useState(() => {
      if (!treeData) {
        return [];
      }
      const selectedKeys = [];
      getSelectedKeysFromTreeModel(treeData, selectedKeys);
      return selectedKeys;
    });

    const handleTreeNodeChange = keys => {
      setSelectedKeys(keys);
      setTreeCheckableState(treeData, keys);
      if (condition) {
        condition.value = treeData;
        onConditionChange && onConditionChange(condition);
      }
    };

    const isChecked = (selectedKeys, eventKey) =>
      selectedKeys.indexOf(eventKey) !== -1;

    const setTreeCheckableState = (treeList?, keys?: string[]) => {
      return (treeList || []).map(c => {
        c.isSelected = isChecked(keys, c.key);
        c.children = setTreeCheckableState(c.children, keys);
        return c;
      });
    };

    return (
      <Tree
        multiple
        checkable
        autoExpandParent
        treeData={treeData as any}
        checkedKeys={selectedKeys}
        onSelect={handleTreeNodeChange}
        onCheck={handleTreeNodeChange}
      />
    );
  },
)
Example #9
Source File: index.tsx    From jetlinks-ui-antd with MIT License 5 votes vote down vote up
{ TreeNode } = Tree
Example #10
Source File: api-doc-tree.tsx    From erda-ui with GNU Affero General Public License v3.0 5 votes vote down vote up
{ TreeNode } = Tree
Example #11
Source File: index.tsx    From gant-design with MIT License 5 votes vote down vote up
{ TreeNode } = Tree
Example #12
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 5 votes vote down vote up
{ DirectoryTree, TreeNode } = Tree
Example #13
Source File: SystemInfoTree.tsx    From disco-cube-admin with MIT License 5 votes vote down vote up
SystemInfoTree: React.FC<Props> = ({ info }) => {
  const [treeData, setTreeData] = React.useState(() => [convertInfoToTreeData(info)]);

  React.useEffect(() => setTreeData([convertInfoToTreeData(info)]), [info]);

  return <Tree showLine={true} defaultExpandedKeys={["0-0-0"]} treeData={treeData} />;
}
Example #14
Source File: SelectForm.tsx    From jetlinks-ui-antd with MIT License 4 votes vote down vote up
SelectForm = (props: Props) => {
  const id = props.data.id || '';
  const [addVisible, setAddVisible] = useState<boolean>(false);
  const [addTableName, setAddTableName] = useState<string>("");
  const [rdbList, setRdbList] = useState<any[]>([]);
  const [dataSource, setDataSource] = useState<any>({});
  const [defaultSelectedKeys, setDefaultSelectedKeys] = useState<any[]>([]);
  const [isEdit, setIsEdit] = useState<boolean>(false);
  const [data, setData] = useState<any>({});

  /**
   * 获取右边表格数据
   */

  const getTableData = (id: string, key: string) => {
    if(!!id){
      service.rdbTables(id, key).subscribe(resp => {
        if(resp.status === 200){
          const list = resp.result.columns.map((item: any) => {
            return {...item, id: item.name}
          })
          setDataSource({
            ...resp.result,
            columns: [...list]
          })
        }
      })
    }
  }

  /**
   * 查询左侧表格列表
   */
  const getTableList = (id: string) => {
    service.rdbTree(id).subscribe(resp => {
      if(resp.status === 200){
        setRdbList(resp.result)
        getTableData(id, resp.result[0].name);
        setDefaultSelectedKeys([rdbList[0]?.name]);
        setIsEdit(false);
      }
    })
  }

  useEffect(() => {
    if(id && id !== ''){
      getTableList(id)
    }
  }, [props.data])

  /**
   * 保存数据
   */
  const saveRdbTables = (data: any) => {
    service.saveRdbTables(id, data).subscribe(resp => {
      if(resp.status === 200){
        message.success('保存成功');
        getTableList(id);
      }
    })
  }

  return (
      <Modal width={1400}
         onCancel={() => {props.save(); setIsEdit(false);}}
         visible
         onOk={() => {
           setIsEdit(false);
           props.save()
         }}
      >
        <Row gutter={24} style={{marginTop: '20px'}}>
          <Col span={6}>
            <div style={{height: '700px', overflowY: "auto"}}>
              <Tree
                showLine
                defaultExpandAll
                selectedKeys={[...defaultSelectedKeys]}
                onSelect={(selectedKeys) => {
                  getTableData(id,selectedKeys[0]);
                  setDefaultSelectedKeys([...selectedKeys])
                }}
              >
                <Tree.TreeNode title={`tables(${rdbList.length})`} key={'tables'}>
                  {
                    rdbList.map(item => (
                      <Tree.TreeNode key={item.name} title={item.name} />
                    ))
                  }
                </Tree.TreeNode>
              </Tree>
            </div>
          </Col>
          <Col span={18}>
            <Card title={dataSource.name ? `表名称:${dataSource.name}` : ''} bordered={false} extra={
              <div>
                <Button icon={'table'} type={'dashed'} onClick={() => {
                  setAddVisible(true);
                  setAddTableName("");
                }}>添加表</Button>
                {
                  dataSource.name && <Button icon={"plus"} style={{margin: '0 20px'}} onClick={() => {
                    const list = [...dataSource.columns]
                    list.push({
                      id: Math.random()*500000000,
                      name: '',
                      type: '',
                      length: 0,
                      scale: 0,
                      notnull: false,
                      comment: ''
                      ,                  })
                    setDataSource({
                      ...dataSource,
                      columns: [...list]
                    })
                  }}>添加列</Button>
                }
                {
                  isEdit && <Button type={'primary'} onClick={() => {
                    saveRdbTables({
                      name: dataSource.name,
                      columns: [...data]
                    });
                    setIsEdit(false);
                  }}>保存</Button>
                }
              </div>
            }>
              <div style={{height: '600px', overflowY: "auto"}}>
                <EditableFormTable 
                  id={id}
                  table={dataSource?.name}
                  save={(data: any) => {
                    const list = data.map((item: any) => {
                      const obj = { ...item }
                      delete obj.id
                      return { ...obj }
                    })
                    setData([...list]);
                    setIsEdit(true);
                  }} data={dataSource?.columns || []} />
              </div>
            </Card>
          </Col>
        </Row>
        <Modal
          title="添加表"
          visible={addVisible}
          onOk={() => {
            if(addTableName){
              rdbList.push({
                name: addTableName
              })
              setRdbList([...rdbList]);
              setDefaultSelectedKeys([addTableName]);
              setDataSource({
                "name":addTableName,
                "columns": []
              })
              setAddVisible(false);
              setAddTableName("");
              setIsEdit(false);
            }else{
              message.error("请输入需要添加的表格的名称")
            }
          }}
          onCancel={() => {
            setAddVisible(false);
          }}
        >
          <Input value={addTableName} onChange={(event) => {
            setAddTableName(event.target.value)
          }} placeholder={"请输入需要添加的表格的名称"} />
        </Modal>
      </Modal>
  );
}
Example #15
Source File: WebsiteTree.tsx    From yakit with GNU Affero General Public License v3.0 4 votes vote down vote up
WebsiteTreeViewer: React.FC<WebsiteTreeViewerProp> = (props) => {
    const [treeData, setTreeData] = useState<AntDTreeData[]>([])
    const [autoRefresh, setAutoRefresh] = useState(!!props.targets)
    const [selected, setSelected] = useState(props.targets)
    const [limit, setLimit] = useState(20)
    const [page, setPage] = useState(1)
    const [total, setTotal] = useState(0)
    const [searchTarget, setSearchTarget] = useState(props.targets)
    const [loading, setLoading] = useState(false)
    const [isDelKey, setisDelKey] = useState("")

    const refresh = () => {
        setLoading(true)
        ipcRenderer
            .invoke("GenerateWebsiteTree", {
                Targets: searchTarget
            })
            .then((data: { TreeDataJson: Uint8Array }) => {
                const treeDataRaw = ConvertWebsiteForestToTreeData(
                    JSON.parse(Buffer.from(data.TreeDataJson).toString("utf8")) as WebsiteForest
                ) as AntDTreeData[]
                setTreeData([...treeDataRaw])
                setLoading(false)
            })
    }

    const delReord = (node: AntDTreeData) => {
        function fetchUrl(node: AntDTreeData, url: string): string {
            if (!!node.parent) {
                const str = `${node.title[0] === "/" ? "" : "/"}${node.title}${url}`
                return fetchUrl(node.parent, str)
            } else {
                return `${node.title}${url}`
            }
        }

        ipcRenderer
            .invoke("delete-http-flow-signle", {URLPrefix: fetchUrl(node, "")})
            .then((res) => {
                refresh()
            })
    }

    const fetchUrl = (data: AntDTreeData | any, arr: string[]) => {
        arr.unshift(data.title.indexOf("/") < 0 && !!data?.parent?.title ? `/${data.title}` : data.title)
        if(data?.parent?.title) fetchUrl(data?.parent, arr)
    }

    // 构建 table
    const uidToNodeMap = new Map<string, AntDTreeData>()
    const viewNode = (node: AntDTreeData) => {
        node.children.map(viewNode)
        uidToNodeMap.set(node.key, node)
    }
    treeData.forEach(viewNode)

    useEffect(() => {
        setSearchTarget(props.targets)
        refresh()
    }, [props.targets])

    useEffect(() => {
        if (!autoRefresh) {
            return
        }

        const id = setInterval(() => {
            if (!autoRefresh) {
                return
            }

            refresh()
        }, 3000)
        return () => {
            clearInterval(id)
        }
    }, [autoRefresh])

    return (
        <>
            <Row gutter={8} style={{height: "100%"}}>
                <Col span={7} style={{height: "100%", overflow: "auto"}}>
                    <Spin spinning={loading}>
                        <Card
                            title={
                                <Space>
                                    业务结构
                                    <Button
                                        type={"link"}
                                        size={"small"}
                                        icon={<ReloadOutlined/>}
                                        onClick={() => {
                                            refresh()
                                        }}
                                    />
                                </Space>
                            }
                            size={"small"}
                            extra={
                                !props.targets ? (
                                    <Space>
                                        <Form
                                            size={"small"}
                                            onSubmitCapture={(e) => {
                                                e.preventDefault()

                                                refresh()
                                            }}
                                            layout={"inline"}
                                        >
                                            <InputItem
                                                label={"URL关键字"}
                                                value={searchTarget}
                                                setValue={setSearchTarget}
                                                width={100}
                                            />
                                            <Form.Item style={{marginLeft: 0, marginRight: 0}}>
                                                <Button
                                                    size={"small"}
                                                    type='link'
                                                    htmlType='submit'
                                                    icon={<SearchOutlined/>}
                                                    style={{marginLeft: 0, marginRight: 0}}
                                                />
                                            </Form.Item>
                                        </Form>
                                        {/*<Input onBlur={r => setSearchTarget(r.target.value)} size={"small"}/>*/}
                                    </Space>
                                ) : (
                                    <Space>
                                        <Form onSubmitCapture={e => {
                                            e.preventDefault()
                                        }} size={"small"}>
                                            <SwitchItem
                                                label={"自动刷新"} formItemStyle={{marginBottom: 0}}
                                                value={autoRefresh} setValue={setAutoRefresh}
                                            />
                                        </Form>
                                    </Space>
                                )
                            }
                        >
                            <div style={{width: "100%", overflowX: "auto", maxHeight: props.maxHeight}}>
                                <Tree
                                    className='ellipsis-tree'
                                    showLine={true}
                                    treeData={treeData}
                                    titleRender={(nodeData: any) => {
                                        return (
                                            <div style={{display: "flex", width: "100%"}}>
                                                <span
                                                    title={`${nodeData.title}`}
                                                    className='titleContent'
                                                >
                                                    {nodeData.title}
                                                </span>
                                                <Popconfirm
                                                    title={"确定要删除该记录吗?本操作不可恢复"}
                                                    onConfirm={(e) => {
                                                        // 阻止冒泡
                                                        e?.stopPropagation()

                                                        delReord(nodeData)
                                                    }}
                                                    onCancel={(e) => {
                                                        // 阻止冒泡
                                                        e?.stopPropagation()
                                                    }}
                                                >
                                                    {isDelKey === nodeData.title && (
                                                        <DeleteOutlined
                                                            style={{
                                                                paddingLeft: 5,
                                                                paddingTop: 5,
                                                                cursor: "pointer",
                                                                color: "#707070"
                                                            }}
                                                            onClick={(e) => {
                                                                // 阻止冒泡
                                                                e.stopPropagation()
                                                            }}
                                                        />
                                                    )}
                                                </Popconfirm>
                                            </div>
                                        )
                                    }}
                                    onSelect={(key) => {
                                        if (key.length <= 0) {
                                            setisDelKey("")
                                            return
                                        }
                                        const selectedKey = key[0]
                                        const node = uidToNodeMap.get(selectedKey as string)
                                        if (!node) {
                                            return
                                        }
                                        let path = [node.title]
                                        setisDelKey(path[0])

                                        let parent = node.parent
                                        while (!!parent) {
                                            path.unshift(
                                                !parent.parent ? parent.title + "/" : parent.title
                                            )
                                            parent = parent.parent
                                        }
                                        const pathStr = (path || []).join("")
                                        setSelected(pathStr)
                                    }}
                                    autoExpandParent={true}
                                    defaultExpandAll={true}
                                    onRightClick={({event, node}) => {
                                        showByContextMenu(
                                            {
                                                data: [
                                                    {key:'bug-test',title:"发送到漏洞检测"},
                                                    {key:'scan-port',title:"发送到端口扫描"},
                                                    {key:'brute',title:"发送到爆破"}
                                                ],
                                                onClick: ({key}) => {
                                                    let str: string[] = []
                                                    fetchUrl(node, str)
                                                    const param = {
                                                        SearchURL: str,
                                                        Pagination: {
                                                            ...genDefaultPagination(20),
                                                            Page: 1,
                                                            Limit: 101
                                                    }}
                                                    ipcRenderer.invoke("QueryHTTPFlows", param).then((data: QueryGeneralResponse<HTTPFlow>) => {
                                                        if(data.Total > 100){
                                                            failed("该节点下的URL数量超过100个,请缩小范围后再重新操作")
                                                            return
                                                        }
                                                        ipcRenderer.invoke("send-to-tab", {
                                                            type: key,
                                                            data:{URL: JSON.stringify(data.Data.map(item => item.Url))}
                                                        })
                                                    })
                                                }
                                            }
                                    )}}
                                />
                            </div>
                        </Card>
                    </Spin>
                </Col>
                <Col span={17} style={{height: "100%"}}>
                    <Card
                        size={"small"}
                        className={"flex-card"}
                        title={"HTTP Flow Record"}
                        bodyStyle={{padding: 0}}
                        extra={
                            <Pagination
                                simple={true}
                                defaultCurrent={1}
                                size={"small"}
                                pageSize={limit}
                                current={page}
                                onChange={(page, size) => {
                                    setPage(page)
                                    setLimit(size || 20)
                                }}
                                total={total}
                            />
                        }
                    >
                        <HTTPFlowMiniTable
                            onTotal={setTotal}
                            filter={{
                                SearchURL: selected,
                                Pagination: {
                                    ...genDefaultPagination(20),
                                    Page: page,
                                    Limit: limit
                                }
                            }}
                            source={""}
                        />
                    </Card>
                </Col>
            </Row>
        </>
    )
}
Example #16
Source File: index.tsx    From jetlinks-ui-antd with MIT License 4 votes vote down vote up
Auth = (props: Props) => {
  const service = new Service('open-api');
  const [treeData, setTreeData] = useState<{ children: any; title: any; key: any }[]>();

  const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
  const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
  const [permissions, setPermissions] = useState<any[]>();

  useEffect(() => {
    const selected: string[] = [];
    zip(
      service.permission.auth(
        encodeQueryParam({
          terms: {
            dimensionTarget: props.current.id,
          },
        }),
      ),
      service.permission.query({}),
    )
      .pipe(
        map(list =>
          list[1].map(item => ({
            ...item,
            children: (item.children || []).map((i: any) => {
              const flag = (list[0].find(j => j.key === item.key)?.actions || []).includes(i.key);
              if (flag) selected.push(`${item.key}:${i.key}`);
              return {
                ...i,
                key: `${item.key}:${i.key}`,
                enabled: flag,
              };
            }),
          })),
        ),
      )
      .subscribe(data => {
        setTreeData(data);
        setExpandedKeys(data.map(item => item.key));
        setCheckedKeys(selected);
        setPermissions(data);
      });
  }, []);

  const onExpand = expandedKeys => {
    setExpandedKeys(expandedKeys);
    setAutoExpandParent(false);
  };

  const onCheck = (keys?: any) => {
    setCheckedKeys(keys);
    const list: { id: string; actions: string[] }[] = [];
    keys
      .filter((i: string) => i.indexOf(':') > 0)
      .forEach((j: string) => {
        const id = j.split(':')[0];
        const action = j.split(':')[1];
        const temp = list.findIndex(i => i.id === id);
        if (temp > -1) {
          list[temp].actions.push(action);
        } else {
          list.push({ id, actions: [action] });
        }
      });
    setPermissions(list);
    return list;
  };

  const onSelect = (selectedKeys, info) => {
    setSelectedKeys(selectedKeys);
  };

  const updateAuth = () => {
    const permissions = onCheck(checkedKeys);
    service.permission
      .save({
        targetId: props.current.id,
        targetType: 'open-api',
        permissionList: permissions,
      })
      .subscribe(() => {
        message.success('保存成功');
      });
  };
  return (
    <Drawer
      title="授权"
      width="50VW"
      visible
      onClose={() => props.close()}
      bodyStyle={{
        overflow: 'auto',
        height: '800px',
      }}
    >
      <Tree
        defaultExpandAll
        checkable
        onExpand={onExpand}
        expandedKeys={expandedKeys}
        autoExpandParent={autoExpandParent}
        onCheck={onCheck}
        checkedKeys={checkedKeys}
        onSelect={onSelect}
        selectedKeys={selectedKeys}
        treeData={treeData}
      />
      <div
        style={{
          position: 'absolute',
          right: 0,
          bottom: 0,
          width: '100%',
          borderTop: '1px solid #e9e9e9',
          padding: '10px 16px',
          background: '#fff',
          textAlign: 'right',
        }}
      >
        <Button
          onClick={() => {
            props.close();
          }}
          style={{ marginRight: 8 }}
        >
          关闭
        </Button>
        <Button
          onClick={() => {
            updateAuth();
          }}
          type="primary"
        >
          保存
        </Button>
      </div>
    </Drawer>
  );
}
Example #17
Source File: index.tsx    From visual-layout with MIT License 4 votes vote down vote up
NodeTree = () => {
  const [search, setSearch] = useState('');
  const [expandedKeys, setExpandedKeys] = useState<Key[]>([]);
  const [autoExpandParent, setAutoExpandParent] = useState(true);
  const { appService, refresh } = useContext(AppContext);

  const page = appService.project.getCurrentPage();
  useEffect(() => {
    setAutoExpandParent(true);
    setExpandedKeys([
      // @ts-ignore
      ...new Set([
        ...expandedKeys,
        ...(page?.currentNode.map(({ id }) => id) || []),
      ]),
    ]);
    // eslint-disable-next-line
  }, [appService.project, page?.currentNode[0]]);

  const trees = useMemo((): DataNode[] => {
    const getTree = (node: NodeService | string, id?: number): DataNode => {
      if (isString(node)) {
        return {
          title: node,
          key: `${id}:${node}`,
          icon: (
            <Tooltip placement="right" title="Text">
              <ProfileOutlined />
            </Tooltip>
          ),
        };
      } else {
        const { id, _name, type, children } = node;
        nodeKeyId.set(id, node);
        return {
          title: `${_name}`,
          key: id,
          icon: ({ selected }) =>
            selected ? (
              <CheckCircleTwoTone twoToneColor="#52c41a" />
            ) : type === 'Component' ? (
              <Tooltip placement="right" title="Component">
                <AppstoreOutlined />
              </Tooltip>
            ) : (
              <Tooltip placement="right" title="Element">
                <BuildOutlined />
              </Tooltip>
            ),

          children: isString(children)
            ? [
                {
                  title: children,
                  key: `${id}:${children}`,
                  icon: (
                    <Tooltip placement="right" title="Text">
                      <ProfileOutlined />
                    </Tooltip>
                  ),
                },
              ]
            : (children
                ?.map(child => child && getTree(child, id))
                .filter(_ => _) as DataNode[]),
        };
      }
    };
    return page?.page ? [getTree(page.page)] : [];
    // eslint-disable-next-line
  }, [refresh, page?.page]);

  const filter = (treeData: DataNode[]): DataNode[] => {
    function matchSearch<T extends string>(title: T): boolean {
      return !search || new RegExp(_.escapeRegExp(search), 'ig').test(title);
    }

    return treeData
      .map(tree => {
        const { title, children } = tree;
        tree.children = children && filter(children);
        if (tree.children?.length || matchSearch(title as string)) {
          return tree;
        }
        return false;
      })
      .filter(_ => _) as DataNode[];
  };

  return (
    <div className={styles.container}>
      <div className={styles.search}>
        <Input.Search
          placeholder="Search Node"
          onChange={e => setSearch(e.target.value)}
        />
      </div>
      <div className={styles.scrollWarper}>
        <Tree
          showIcon
          onSelect={(_, { node }) => {
            if (node) {
              const nodeService = nodeKeyId.get(node.key);
              if (nodeService) {
                page.setCurrentNode([nodeService]);
              }
            }
          }}
          showLine={{ showLeafIcon: false }}
          selectedKeys={
            appService.project.getCurrentPage()?.currentNode.map(({ id }) => id) ||
            []
          }
          autoExpandParent={autoExpandParent}
          expandedKeys={expandedKeys}
          onExpand={expandedKeysValue => {
            setAutoExpandParent(false);
            setExpandedKeys(expandedKeysValue);
          }}
          treeData={filter(cloneJsxObject(trees))}
        />
      </div>
    </div>
  );
}
Example #18
Source File: GroupPanelTree.tsx    From tailchat with GNU General Public License v3.0 4 votes vote down vote up
GroupPanelTree: React.FC<GroupPanelTree> = React.memo((props) => {
  const treeData: DataNode[] = useMemo(
    () => buildTreeDataWithGroupPanel(props.groupPanels),
    [props.groupPanels]
  );

  const handleModifyPanel = useCallback(
    (panelId: string) => {
      const key = openModal(
        <ModalModifyGroupPanel
          groupId={props.groupId}
          groupPanelId={panelId}
          onSuccess={() => closeModal(key)}
        />
      );
    },
    [props.groupId]
  );

  const handleDeletePanel = useCallback(
    (panelId: string, panelName: string, isGroup: boolean) => {
      showAlert({
        message: isGroup
          ? t('确定要删除面板组 【{{name}}】 以及下级的所有面板么', {
              name: panelName,
            })
          : t('确定要删除面板 【{{name}}】 么', { name: panelName }),
        onConfirm: async () => {
          await deleteGroupPanel(props.groupId, panelId);
        },
      });
    },
    [props.groupId]
  );

  const titleRender = useCallback(
    (node: DataNode): React.ReactNode => {
      return (
        <div className="flex group">
          <span>{node.title}</span>
          <div className="opacity-0 group-hover:opacity-100 ml-2">
            <Space size="small">
              <IconBtn
                title={t('编辑')}
                type="text"
                size="small"
                icon="mdi:pencil-outline"
                onClick={(e) => {
                  e.stopPropagation();
                  e.preventDefault();
                  handleModifyPanel(String(node.key));
                }}
              />

              <IconBtn
                title={t('删除')}
                type="text"
                size="small"
                icon="mdi:trash-can-outline"
                onClick={(e) => {
                  e.stopPropagation();
                  e.preventDefault();
                  handleDeletePanel(
                    String(node.key),
                    String(node.title),
                    !node.isLeaf
                  );
                }}
              />
            </Space>
          </div>
        </div>
      );
    },
    [handleDeletePanel]
  );

  const { handleDragStart, handleDragEnd, handleAllowDrop, handleDrop } =
    useGroupPanelTreeDrag(props.groupPanels, props.onChange);

  return (
    <Tree
      treeData={treeData}
      defaultExpandAll={true}
      blockNode={true}
      draggable={{
        icon: false,
      }}
      selectable={false}
      titleRender={titleRender}
      onDrop={handleDrop}
      // TODO: 待简化 https://github.com/react-component/tree/pull/482
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}
      allowDrop={handleAllowDrop}
    />
  );
})
Example #19
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
DepartmentSelectionModal: React.FC<DepartmentSelectionProps> = (props) => {
  const {visible, setVisible, defaultCheckedDepartments, onFinish, allDepartments} = props;
  const [departments, setDepartments] = useState<DepartmentOption[]>([]);
  const [departmentNodes, setDepartmentNodes] = useState<TreeNode[]>([]); // 一维的节点
  const [departmentTree, setDepartmentTree] = useState<TreeNode[]>([]); // 多维的树节点
  const [selectedDepartments, setSelectedDepartments] = useState<DepartmentOption[]>(
    _.filter<DepartmentOption>(defaultCheckedDepartments) || [],
  );
  const [keyword, setKeyword] = useState<string>('');
  const [checkAll, setCheckAll] = useState<boolean>(false);
  const [expandAll, setExpandAll] = useState<boolean>(false);
  const [checkedNodeKeys, setCheckedNodeKeys] = useState<string[]>([]);
  const [expandedNodeKeys, setExpandedNodeKeys] = useState<string[]>(['1']);
  const allDepartmentMap = _.keyBy(allDepartments, 'ext_id');

  const onCheckAllChange = (e: CheckboxChangeEvent) => {
    let items: DepartmentOption[];
    if (e.target.checked) {
      items = _.uniqWith<DepartmentOption>(
        [...departments, ...selectedDepartments],
        (a, b) => a.ext_id === b.ext_id,
      );
    } else {
      items = _.differenceWith(selectedDepartments, departments, (a, b) => a.ext_id === b.ext_id);
    }
    setSelectedDepartments(items);
    setCheckAll(e.target.checked);
  };

  const onNodesCheck = (checked: { checked: string[]; halfChecked: string[] }) => {
    const checkedExtDepartmentIDs: number[] = [];
    let selectedExtDepartmentIDs = selectedDepartments.map((item) => item.ext_id);
    let checkedKeys = [...checked.checked];

    // 找出本次uncheck的key,根据这些key的ext_id去删除相关checkedKey
    const uncheckedKeys = _.difference(checkedNodeKeys, checkedKeys);
    _.forEach<string>(uncheckedKeys, (key: string) => {
      // @ts-ignore
      checkedKeys = checkedKeys.filter<string>((checkedKey) => {
        return !checkedKey.includes(key);
      });
    });

    // 记录当前所有checked的key
    checkedKeys.forEach((key) => {
      checkedExtDepartmentIDs.push(Number(key));
      selectedExtDepartmentIDs.push(Number(key));
    });

    // 计算需要删除的extDepartmentID
    // @ts-ignore
    const shouldDeleteExtDepartmentIDs = _.difference(
      _.map(departments, 'ext_id'),
      checkedExtDepartmentIDs,
    );
    selectedExtDepartmentIDs = _.difference(
      _.uniq(selectedExtDepartmentIDs),
      _.uniq(shouldDeleteExtDepartmentIDs),
    );

    const items = selectedExtDepartmentIDs.map((selectedExtDepartmentID) => {
      return allDepartmentMap[selectedExtDepartmentID];
    });

    setCheckAll(departments.length === items.length);
    setSelectedDepartments(items);
  };

  const nodeRender = (node: DataNode): ReactNode => {
    return (
      <>
        <FolderFilled
          style={{
            color: '#47a7ff',
            fontSize: 20,
            marginRight: 6,
            verticalAlign: -6,
          }}
        />
        {node.title}
      </>
    );
  };

  useEffect(() => {
    setSelectedDepartments(_.filter<DepartmentOption>(defaultCheckedDepartments) || []);
    setKeyword('');
  }, [defaultCheckedDepartments, visible]);

  // 监听选中部门变化,计算checked的树节点
  useEffect(() => {
    const allDepartmentNodeKeys = _.map(departmentNodes, 'key');
    // 计算当前选中的部门,命中的key
    const matchedKeys: string[] = [];
    allDepartmentNodeKeys.forEach((key: string) => {
      selectedDepartments.forEach((department) => {
        if (key === `${department.ext_id}`) {
          matchedKeys.push(key);
        }
      });
    });
    setCheckedNodeKeys(matchedKeys);
  }, [selectedDepartments]);

  // 关键词变化的时候
  useEffect(() => {
    let filteredDepartments: DepartmentOption[] = [];

    allDepartments.forEach((item) => {
      if (keyword.trim() === '' || item.label.includes(keyword.trim())) {
        filteredDepartments.push(item);
      }
    })

    // 把搜索结果的父级节点放进结果集,方便构造树
    const pushParentDepartment = (item: DepartmentOption) => {
      if (item.ext_parent_id === 0) {
        filteredDepartments.push(item);
        return;
      }
      const parent = allDepartmentMap[item.ext_parent_id];
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      filteredDepartments.push(parent);
      pushParentDepartment(parent);
    };

    filteredDepartments.forEach((item) => {
      pushParentDepartment(item);
    })

    filteredDepartments = _.uniq<DepartmentOption>(filteredDepartments);
    setDepartments(filteredDepartments);

    const {nodes, tree} = buildDepartmentTree(filteredDepartments);

    // 这里同步更新node节点和选中key值
    let checkedKeys: string[] = [];
    nodes.forEach((node) => {
      selectedDepartments.forEach((department) => {
        if (node.key === `${department.ext_id}`) {
          checkedKeys.push(node.key);
        }
      });
    });
    checkedKeys = _.uniq<string>(checkedKeys);
    setCheckedNodeKeys(checkedKeys);

    setCheckAll(false);
    setDepartmentNodes(nodes);
    setDepartmentTree(tree);
  }, [allDepartments, keyword]);

  // @ts-ignore
  return (
    <Modal
      width={665}
      className={'dialog from-item-label-100w'}
      visible={visible}
      zIndex={1001}
      onCancel={() => setVisible(false)}
      onOk={() => {
        if (onFinish) {
          onFinish(selectedDepartments);
        }
        setVisible(false);
      }}
    >
      <h2 className="dialog-title"> 选择部门 </h2>
      <div className={styles.addDepartmentDialogContent}>
        <div className={styles.container}>
          <div className={styles.left}>
            <p className={styles.toolTop} style={{marginBottom: 0}}>
              <Search
                className={styles.searchInput}
                enterButton={'搜索'}
                prefix={<SearchOutlined/>}
                placeholder="请输入部门名称"
                allowClear
                value={keyword}
                onChange={(e) => {
                  setKeyword(e.target.value);
                }}
              />
            </p>
            <p style={{marginBottom: 0}}>
              <Checkbox checked={checkAll} onChange={onCheckAllChange}>
                全部部门({departments.length}):
              </Checkbox>
              <Button
                type={'link'}
                onClick={() => {
                  const currentStatus = !expandAll;
                  if (currentStatus) {
                    setExpandedNodeKeys(_.map(departmentNodes, 'key'));
                  } else {
                    setExpandedNodeKeys(['0']);
                  }
                  setExpandAll(currentStatus);
                }}
                style={{marginRight: 30}}
              >
                {!expandAll ? '展开全部' : '收起全部'}
              </Button>
            </p>
            <div className={styles.allDepartment}>
              {departmentTree.length === 0 && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>}
              <Tree
                className={styles.departmentTree}
                autoExpandParent={false}
                checkStrictly={true}
                checkedKeys={checkedNodeKeys}
                defaultExpandedKeys={checkedNodeKeys}
                expandedKeys={expandedNodeKeys}
                // @ts-ignore
                onExpand={(expandedKeys: string[]) => {
                  setExpandedNodeKeys(expandedKeys);
                }}
                height={300}
                switcherIcon={<CaretDownFilled style={{color: '#47a7ff'}}/>}
                checkable={true}
                multiple={true}
                treeData={departmentTree}
                // @ts-ignore
                onCheck={onNodesCheck}
                titleRender={nodeRender}
              />
            </div>
          </div>
          <div className={styles.right}>
            <p>
              已选部门({selectedDepartments.length}):
              <Button
                type={'link'}
                onClick={() => {
                  setSelectedDepartments([]);
                  setCheckAll(false);
                }}
              >
                清空
              </Button>
            </p>
            <ul className={styles.allDepartmentList}>
              {selectedDepartments.map((department) => {
                if (!department) {
                  return <></>
                }

                return (
                  <li
                    key={department.ext_id}
                    onClick={() => {
                      setSelectedDepartments(
                        selectedDepartments.filter((item) => item.ext_id !== department.ext_id),
                      );
                    }}
                  >
                    <div className={styles.avatarAndName}>
                      <div className="flex-col align-left">
                        <FolderFilled
                          style={{
                            color: '#47a7ff',
                            fontSize: 20,
                            marginRight: 6,
                            verticalAlign: -6,
                          }}
                        />
                        {department.name}
                      </div>
                    </div>
                    <CloseOutlined/>
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      </div>
    </Modal>
  );
}
Example #20
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
StaffTreeSelectionModal: React.FC<StaffSelectionProps> = (props) => {
  const { visible, setVisible, defaultCheckedStaffs, onFinish, allStaffs } = props;
  const [staffs, setStaffs] = useState<StaffOption[]>([]);
  const [staffNodes, setStaffNodes] = useState<TreeNode[]>([]); // 一维的节点
  const [staffTree, setStaffTree] = useState<TreeNode[]>([]); // 多维的树节点
  const [selectedStaffs, setSelectedStaffs] = useState<StaffOption[]>(defaultCheckedStaffs || []);
  const [keyword, setKeyword] = useState<string>('');
  const [checkAll, setCheckAll] = useState<boolean>(false);
  const [expandAll, setExpandAll] = useState<boolean>(false);
  const [checkedNodeKeys, setCheckedNodeKeys] = useState<string[]>([]);
  const [expandedNodeKeys, setExpandedNodeKeys] = useState<string[]>([rootNode]);
  const allStaffMap = _.keyBy(allStaffs, 'ext_id');

  const onCheckAllChange = (e: CheckboxChangeEvent) => {
    let items: StaffOption[];
    if (e.target.checked) {
      items = _.uniqWith<StaffOption>(
        [...staffs, ...selectedStaffs],
        (a, b) => a.ext_id === b.ext_id,
      );
    } else {
      // @ts-ignore
      items = _.differenceWith<StaffParam>(selectedStaffs, staffs, (a, b) => a.ext_id === b.ext_id);
    }
    setSelectedStaffs(items);
    setCheckAll(e.target.checked);
  };

  const onNodesCheck = (checked: string[]) => {
    const checkedExtStaffIDs: string[] = [];
    let selectedExtStaffIDs = _.map(selectedStaffs, 'ext_id');
    let checkedKeys = [...checked];

    // 找出本次uncheck的key,根据这些key的ext_id去删除相关checkedKey
    const uncheckedKeys = _.difference(checkedNodeKeys, checkedKeys);
    _.forEach<string>(uncheckedKeys, (key: string) => {
      const [, , nodeID] = key.split(separator);
      // eslint-disable-next-line no-param-reassign
      // @ts-ignore
      checkedKeys = checkedKeys.filter<string>((checkedKey) => {
        return !checkedKey.includes(`${separator}${nodeID}`);
      });
    });

    // 记录当前所有checked的key
    checkedKeys.forEach((key) => {
      const [nodeType, , nodeID] = key.split(separator);
      if (nodeType === 'node') {
        checkedExtStaffIDs.push(nodeID);
        selectedExtStaffIDs.push(nodeID);
      }
    });

    // 计算需要删除的extStaffID
    // @ts-ignore
    const shouldDeleteExtStaffIDs = _.difference(_.map(staffs, 'ext_id'), checkedExtStaffIDs);
    selectedExtStaffIDs = _.difference(
      _.uniq(selectedExtStaffIDs),
      _.uniq(shouldDeleteExtStaffIDs),
    );

    const items = selectedExtStaffIDs.map((selectedExtStaffID) => {
      return allStaffMap[selectedExtStaffID];
    });

    setCheckAll(staffs.length === items.length);
    setSelectedStaffs(items);
  };

  const nodeRender = (node: DataNode): ReactNode => {
    const [nodeType, , extStaffID] = node.key.toString().split(separator);
    if (nodeType === 'node') {
      const staff = allStaffMap[extStaffID];
      if (staff) {
        return (
          <div className={styles.staffTitleNode}>
            <img src={FormatWeWorkAvatar(staff?.avatar_url, 60)} className={styles.avatar} />
            <div className={styles.text}>
              <span className={styles.title}>{staff?.name}</span>
              <em
                style={{
                  color: RoleColorMap[staff?.role_type],
                  borderColor: RoleColorMap[staff?.role_type],
                }}
              >
                {RoleMap[staff?.role_type] || '普通员工'}
              </em>
            </div>
          </div>
        );
      }
    }
    return (
      <>
        <FolderFilled
          style={{
            color: '#47a7ff',
            fontSize: 20,
            marginRight: 6,
            verticalAlign: -3,
          }}
        />
        {node.title}
      </>
    );
  };

  useEffect(() => {
    setSelectedStaffs(defaultCheckedStaffs || []);
    setKeyword('');
  }, [defaultCheckedStaffs, visible]);

  // 监听选中员工变化,计算checked的树节点
  useEffect(() => {
    const allStaffNodeKeys = _.map(staffNodes, 'key');
    // 计算当前选中的员工,命中的key
    const matchedKeys: string[] = [];
    allStaffNodeKeys.forEach((key: string) => {
      selectedStaffs.forEach((staff) => {
        if (key.includes(`${separator}${staff?.ext_id}`)) {
          matchedKeys.push(key);
        }
      });
    });
    setCheckedNodeKeys(matchedKeys);
  }, [selectedStaffs, staffNodes]);

  // 关键词变化的时候
  useEffect(() => {
    const filteredStaffs = allStaffs.filter((item) => {
      // 搜索部门名称
      let isDepartmentMatch = false;
      item?.departments?.forEach((department) => {
        if (department.name.includes(keyword)) {
          isDepartmentMatch = true;
        }
      });
      return keyword === '' || isDepartmentMatch || item.label.includes(keyword);
    });
    setStaffs(filteredStaffs);
    const { nodes, tree } = buildStaffTree(filteredStaffs);

    // 这里同步更新node节点和选中key值
    let checkedKeys: string[] = [];
    nodes.forEach((node) => {
      selectedStaffs.forEach((staff) => {
        if (node.nodeKey.includes(`${separator}${staff?.ext_id}`)) {
          checkedKeys.push(node.key);
        }
      });
    });
    checkedKeys = _.uniq<string>(checkedKeys);
    setCheckedNodeKeys(checkedKeys);

    setCheckAll(false);
    setStaffNodes(nodes);
    setStaffTree(tree);
  }, [allStaffs, keyword]);

  // @ts-ignore
  return (
    <Modal
      width={665}
      className={'dialog from-item-label-100w'}
      visible={visible}
      zIndex={1001}
      onCancel={() => setVisible(false)}
      onOk={() => {
        if (onFinish) {
          onFinish(selectedStaffs);
        }
        setVisible(false);
      }}
    >
      <h2 className='dialog-title'> 选择员工 </h2>
      <div className={styles.addStaffDialogContent}>
        <div className={styles.container}>
          <div className={styles.left}>
            <p className={styles.toolTop} style={{ marginBottom: 0 }}>
              <Search
                className={styles.searchInput}
                enterButton={'搜索'}
                prefix={<SearchOutlined />}
                placeholder='请输入员工昵称'
                allowClear
                value={keyword}
                onChange={(e) => {
                  setKeyword(e.target.value);
                }}
              />
            </p>
            <p style={{ marginBottom: 0 }}>
              <Checkbox checked={checkAll} onChange={onCheckAllChange}>
                全部成员({staffs.length}):
              </Checkbox>
              <Button
                type={'link'}
                onClick={() => {
                  const currentStatus = !expandAll;
                  if (currentStatus) {
                    setExpandedNodeKeys(
                      _.map(
                        staffNodes.filter((staffNode) => staffNode.type === 'group'),
                        'key',
                      ),
                    );
                  } else {
                    setExpandedNodeKeys([rootNode]);
                  }
                  setExpandAll(currentStatus);
                }}
                style={{ marginRight: 30 }}
              >
                {!expandAll ? '展开部门' : '收起部门'}
              </Button>
            </p>
            <div className={styles.allStaff}>
              {staffTree.length === 0 && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
              <Tree
                className={styles.staffTree}
                autoExpandParent={true}
                checkedKeys={checkedNodeKeys}
                defaultExpandedKeys={checkedNodeKeys}
                expandedKeys={expandedNodeKeys}
                // @ts-ignore
                onExpand={(expandedKeys: string[]) => {
                  setExpandedNodeKeys(expandedKeys);
                }}
                height={300}
                switcherIcon={<CaretDownFilled style={{ color: '#47a7ff' }} />}
                checkable={true}
                multiple={true}
                treeData={staffTree}
                // @ts-ignore
                onCheck={onNodesCheck}
                titleRender={nodeRender}
              />
            </div>
          </div>
          <div className={styles.right}>
            <p>
              已选成员({selectedStaffs.length}):
              <Button
                type={'link'}
                onClick={() => {
                  setSelectedStaffs([]);
                  setCheckAll(false);
                }}
              >
                清空
              </Button>
            </p>
            <ul className={styles.allStaffList}>
              {selectedStaffs.map((staff) => {
                if (!staff?.ext_id) {
                  return '';
                }

                return (
                  <li
                    key={staff?.ext_id}
                    onClick={() => {
                      setSelectedStaffs(
                        selectedStaffs.filter((item) => item?.ext_id !== staff?.ext_id),
                      );
                    }}
                  >
                    <div className={styles.avatarAndName}>
                      <img src={staff?.avatar_url} className={styles.avatar} />
                      <div className='flex-col align-left'>
                        <span>{staff?.name}</span>
                      </div>
                    </div>
                    <CloseOutlined />
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      </div>
    </Modal>
  );
}
Example #21
Source File: DepartmentTree.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
DepartMentTreeComp = ({ callback }: { callback: (selectedkey: string) => void }) => {
  const [allDepartments, setAllDepartments] = useState<DepartmentList.Item[]>([]);
  const [departments, setDepartments] = useState<DepartmentOption[]>([]);
  const [departmentNodes, setDepartmentNodes] = useState<TreeNode[]>([]); // 一维的节点
  const [departmentTree, setDepartmentTree] = useState<TreeNode[]>([]); // 多维的树节点
  const [selectedDepartments, setSelectedDepartments] = useState<DepartmentOption[]>([]);
  const [keyword] = useState<string>('');
  const [expandAll, setExpandAll] = useState<boolean>(false);
  const [checkedNodeKeys, setCheckedNodeKeys] = useState<string[]>([]);
  const [expandedNodeKeys, setExpandedNodeKeys] = useState<string[]>(['1']);
  const allDepartmentMap = _.keyBy(allDepartments, 'ext_id');

  useEffect(() => {
    QueryDepartmentList({ page_size: 5000 })
      .then((res: any) => {
        if (res?.code === 0 && res?.data && res?.data.items) {
          setAllDepartments(res?.data.items);
          setExpandedNodeKeys(['1']);
        }
      })
      .catch((err) => {
        message.error(err);
      });
  }, []);

  const onNodesCheck = (checked: { checked: string[]; halfChecked: string[] }) => {
    const checkedExtDepartmentIDs: number[] = [];
    let selectedExtDepartmentIDs = selectedDepartments.map((item) => item.ext_id);
    let checkedKeys = [...checked.checked];

    // 找出本次uncheck的key,根据这些key的ext_id去删除相关checkedKey
    const uncheckedKeys = _.difference(checkedNodeKeys, checkedKeys);
    _.forEach<string>(uncheckedKeys, (key: string) => {
      // @ts-ignore
      checkedKeys = checkedKeys.filter<string>((checkedKey) => {
        return !checkedKey.includes(key);
      });
    });

    // 记录当前所有checkedkey
    checkedKeys.forEach((key) => {
      checkedExtDepartmentIDs.push(Number(key));
      selectedExtDepartmentIDs.push(Number(key));
    });

    // 计算需要删除的extDepartmentID
    // @ts-ignore
    const shouldDeleteExtDepartmentIDs = _.difference(
      _.map(departments, 'ext_id'),
      checkedExtDepartmentIDs,
    );
    selectedExtDepartmentIDs = _.difference(
      _.uniq(selectedExtDepartmentIDs),
      _.uniq(shouldDeleteExtDepartmentIDs),
    );

    const items = selectedExtDepartmentIDs.map((selectedExtDepartmentID) => {
      return allDepartmentMap[selectedExtDepartmentID];
    });

    // @ts-ignore
    setSelectedDepartments(items);
  };

  const nodeRender = (node: DataNode): ReactNode => {
    return (
      <div
        onClick={() => {
          // eslint-disable-next-line @typescript-eslint/no-unused-expressions
          callback && callback(String(node.key))
        }}
        style={{ padding: '4px 6px', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow:'ellipsis'}}
      >
        <FolderFilled
          style={{
            color: '#47a7ff',
            fontSize: 20,
            marginRight: 6,
            verticalAlign: -6,
          }}
        />
        <span>
          {/* node.title */}
          {
            // @ts-ignore
            node.title.length>14? <span>{node.title.slice(0,13)}...</span>:<span>{node.title}</span>
          }
          ({node.staff_num})
        </span>
      </div>
    );
  };
  // 监听选中部门变化,计算checked的树节点
  useEffect(() => {
    const allDepartmentNodeKeys = _.map(departmentNodes, 'key');
    // 计算当前选中的部门,命中的key
    const matchedKeys: string[] = [];
    allDepartmentNodeKeys.forEach((key: string) => {
      selectedDepartments.forEach((department) => {
        if (key === `${department.ext_id}`) {
          matchedKeys.push(key);
        }
      });
    });
    setCheckedNodeKeys(matchedKeys);
  }, [selectedDepartments]);

  // 关键词变化的时候
  useEffect(() => {
    const filteredDepartments = allDepartments.filter((item) => {
      return keyword === '' || item.label.includes(keyword);
    });
    // @ts-ignore
    setDepartments(filteredDepartments);
    const { nodes, tree } = buildDepartmentTree(filteredDepartments);
    // 这里同步更新node节点和选中keylet checkedKeys: string[] = [];
    nodes.forEach((node) => {
      selectedDepartments.forEach((department) => {
        if (node.key === `${department.ext_id}`) {
          checkedKeys.push(node.key);
        }
      });
    });
    checkedKeys = _.uniq<string>(checkedKeys);
    setCheckedNodeKeys(checkedKeys);
    setDepartmentNodes(nodes);
    setDepartmentTree(tree);
  }, [allDepartments, keyword]);

  return (
    <div >
      <div className={styles.header}>
        <span className={styles.departmentTitle}>部门信息</span>
        <a
          type={'link'}
          onClick={() => {
            const currentStatus = !expandAll;
            if (currentStatus) {
              setExpandedNodeKeys(_.map(departmentNodes, 'key'));
            } else {
              setExpandedNodeKeys(['0']);
            }
            setExpandAll(currentStatus);
          }}
        >
          {!expandAll ? '展开全部' : '收起全部'}
        </a>
      </div>
      <div className={styles.treeContainer}>
        <Tree
          autoExpandParent={false}
          checkStrictly={true}
          checkedKeys={checkedNodeKeys}
          expandedKeys={expandedNodeKeys}
          // @ts-ignore
          onExpand={(expandedKeys: string[]) => {
            setExpandedNodeKeys(expandedKeys);
          }}
          height={300}
          switcherIcon={<CaretDownFilled style={{ color: '#47a7ff' }} />}
          multiple={true}
          treeData={departmentTree}
          // @ts-ignore
          onCheck={onNodesCheck}
          titleRender={nodeRender}
        />
      </div>
    </div>
  );
}
Example #22
Source File: CategoryConditionConfiguration.tsx    From datart with Apache License 2.0 4 votes vote down vote up
CategoryConditionConfiguration: ForwardRefRenderFunction<
  FilterOptionForwardRef,
  {
    colName: string;
    dataView?: ChartDataView;
    condition?: ChartFilterCondition;
    onChange: (condition: ChartFilterCondition) => void;
    fetchDataByField?: (fieldId) => Promise<string[]>;
  } & I18NComponentProps
> = (
  {
    colName,
    i18nPrefix,
    condition,
    dataView,
    onChange: onConditionChange,
    fetchDataByField,
  },
  ref,
) => {
  const t = useI18NPrefix(i18nPrefix);
  const [curTab, setCurTab] = useState<FilterConditionType>(() => {
    if (
      [
        FilterConditionType.List,
        FilterConditionType.Condition,
        FilterConditionType.Customize,
      ].includes(condition?.type!)
    ) {
      return condition?.type!;
    }
    return FilterConditionType.List;
  });
  const [targetKeys, setTargetKeys] = useState<string[]>(() => {
    let values;
    if (condition?.operator === FilterSqlOperator.In) {
      values = condition?.value;
      if (Array.isArray(condition?.value)) {
        const firstValues =
          (condition?.value as [])?.filter(n => {
            if (IsKeyIn(n as RelationFilterValue, 'key')) {
              return (n as RelationFilterValue).isSelected;
            }
            return false;
          }) || [];
        values = firstValues?.map((n: RelationFilterValue) => n.key);
      }
    }
    return values || [];
  });
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  const [isTree, setIsTree] = useState(isTreeModel(condition?.value));
  const [treeOptions, setTreeOptions] = useState<string[]>([]);
  const [listDatas, setListDatas] = useState<RelationFilterValue[]>([]);
  const [treeDatas, setTreeDatas] = useState<RelationFilterValue[]>([]);

  useImperativeHandle(ref, () => ({
    onValidate: (args: ChartFilterCondition) => {
      if (isEmpty(args?.operator)) {
        return false;
      }
      if (args?.operator === FilterSqlOperator.In) {
        return !isEmptyArray(args?.value);
      } else if (
        [
          FilterSqlOperator.Contain,
          FilterSqlOperator.PrefixContain,
          FilterSqlOperator.SuffixContain,
          FilterSqlOperator.Equal,
          FilterSqlOperator.NotContain,
          FilterSqlOperator.NotPrefixContain,
          FilterSqlOperator.NotSuffixContain,
          FilterSqlOperator.NotEqual,
        ].includes(args?.operator as FilterSqlOperator)
      ) {
        return !isEmpty(args?.value);
      } else if (
        [FilterSqlOperator.Null, FilterSqlOperator.NotNull].includes(
          args?.operator as FilterSqlOperator,
        )
      ) {
        return true;
      }
      return false;
    },
  }));

  useMount(() => {
    if (curTab === FilterConditionType.List) {
      handleFetchData();
    }
  });

  const getDataOptionFields = () => {
    return dataView?.meta || [];
  };

  const isChecked = (selectedKeys, eventKey) =>
    selectedKeys.indexOf(eventKey) !== -1;

  const fetchNewDataset = async (viewId, colName: string) => {
    const fieldDataset = await getDistinctFields(
      viewId,
      [colName],
      undefined,
      undefined,
    );
    return fieldDataset;
  };

  const setListSelectedState = (
    list?: RelationFilterValue[],
    keys?: string[],
  ) => {
    return (list || []).map(c =>
      Object.assign(c, { isSelected: isChecked(keys, c.key) }),
    );
  };

  const setTreeCheckableState = (
    treeList?: RelationFilterValue[],
    keys?: string[],
  ) => {
    return (treeList || []).map(c => {
      c.isSelected = isChecked(keys, c.key);
      c.children = setTreeCheckableState(c.children, keys);
      return c;
    });
  };

  const handleGeneralListChange = async selectedKeys => {
    const items = setListSelectedState(listDatas, selectedKeys);
    setTargetKeys(selectedKeys);
    setListDatas(items);

    const generalTypeItems = items?.filter(i => i.isSelected);
    const filter = new ConditionBuilder(condition)
      .setOperator(FilterSqlOperator.In)
      .setValue(generalTypeItems)
      .asGeneral();
    onConditionChange(filter);
  };

  const filterGeneralListOptions = useCallback(
    (inputValue, option) => option.label?.includes(inputValue) || false,
    [],
  );

  const handleGeneralTreeChange = async treeSelectedKeys => {
    const selectedKeys = treeSelectedKeys.checked;
    const treeItems = setTreeCheckableState(treeDatas, selectedKeys);
    setTargetKeys(selectedKeys);
    setTreeDatas(treeItems);
    const filter = new ConditionBuilder(condition)
      .setOperator(FilterSqlOperator.In)
      .setValue(treeItems)
      .asTree();
    onConditionChange(filter);
  };

  const onSelectChange = (
    sourceSelectedKeys: string[],
    targetSelectedKeys: string[],
  ) => {
    const newSelectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys];
    setSelectedKeys(newSelectedKeys);
  };

  const handleTreeOptionChange = (
    associateField: string,
    labelField: string,
  ) => {
    setTreeOptions([associateField, labelField]);
  };

  const handleFetchData = () => {
    fetchNewDataset?.(dataView?.id, colName).then(dataset => {
      if (isTree) {
        // setTreeDatas(convertToTree(dataset?.columns, selectedKeys));
        // setListDatas(convertToList(dataset?.columns, selectedKeys));
      } else {
        setListDatas(convertToList(dataset?.rows, selectedKeys));
      }
    });
  };

  const convertToList = (collection, selectedKeys) => {
    const items: string[] = (collection || []).flatMap(c => c);
    const uniqueKeys = Array.from(new Set(items));
    return uniqueKeys.map(item => ({
      key: item,
      label: item,
      isSelected: selectedKeys.includes(item),
    }));
  };

  const convertToTree = (collection, selectedKeys) => {
    const associateField = treeOptions?.[0];
    const labelField = treeOptions?.[1];

    if (!associateField || !labelField) {
      return [];
    }

    const associateKeys = Array.from(
      new Set(collection?.map(c => c[associateField])),
    );
    const treeNodes = associateKeys
      .map(key => {
        const associateItem = collection?.find(c => c[colName] === key);
        if (!associateItem) {
          return null;
        }
        const associateChildren = collection
          .filter(c => c[associateField] === key)
          .map(c => {
            const itemKey = c[labelField];
            return {
              key: itemKey,
              label: itemKey,
              isSelected: isChecked(selectedKeys, itemKey),
            };
          });
        const itemKey = associateItem?.[colName];
        return {
          key: itemKey,
          label: itemKey,
          isSelected: isChecked(selectedKeys, itemKey),
          children: associateChildren,
        };
      })
      .filter(i => Boolean(i)) as RelationFilterValue[];
    return treeNodes;
  };

  const handleTabChange = (activeKey: string) => {
    const conditionType = +activeKey;
    setCurTab(conditionType);
    const filter = new ConditionBuilder(condition)
      .setOperator(null!)
      .setValue(null)
      .asFilter(conditionType);
    setTreeDatas([]);
    setTargetKeys([]);
    setListDatas([]);
    onConditionChange(filter);
  };

  return (
    <StyledTabs activeKey={curTab.toString()} onChange={handleTabChange}>
      <Tabs.TabPane
        tab={t('general')}
        key={FilterConditionType.List.toString()}
      >
        <Row>
          <Space>
            <Button type="primary" onClick={handleFetchData}>
              {t('load')}
            </Button>
            {/* <Checkbox
                checked={isTree}
                disabled
                onChange={e => setIsTree(e.target.checked)}
              >
                {t('useTree')}
              </Checkbox> */}
          </Space>
        </Row>
        <Row>
          <Space>
            {isTree && (
              <>
                {t('associateField')}
                <Select
                  value={treeOptions?.[0]}
                  options={getDataOptionFields()?.map(f => ({
                    label: f.name,
                    value: f.id,
                  }))}
                  onChange={value =>
                    handleTreeOptionChange(value, treeOptions?.[1])
                  }
                />
                {t('labelField')}
                <Select
                  value={treeOptions?.[1]}
                  options={getDataOptionFields()?.map(f => ({
                    label: f.name,
                    value: f.id,
                  }))}
                  onChange={value =>
                    handleTreeOptionChange(treeOptions?.[0], value)
                  }
                />
              </>
            )}
          </Space>
        </Row>
        {isTree && (
          <Tree
            blockNode
            checkable
            checkStrictly
            defaultExpandAll
            checkedKeys={targetKeys}
            treeData={treeDatas}
            onCheck={handleGeneralTreeChange}
            onSelect={handleGeneralTreeChange}
          />
        )}
        {!isTree && (
          <Transfer
            operations={[t('moveToRight'), t('moveToLeft')]}
            dataSource={listDatas}
            titles={[`${t('sourceList')}`, `${t('targetList')}`]}
            targetKeys={targetKeys}
            selectedKeys={selectedKeys}
            onChange={handleGeneralListChange}
            onSelectChange={onSelectChange}
            render={item => item.label}
            filterOption={filterGeneralListOptions}
            showSearch
            pagination
          />
        )}
      </Tabs.TabPane>
      <Tabs.TabPane
        tab={t('customize')}
        key={FilterConditionType.Customize.toString()}
      >
        <CategoryConditionEditableTable
          dataView={dataView}
          i18nPrefix={i18nPrefix}
          condition={condition}
          onConditionChange={onConditionChange}
          fetchDataByField={fetchDataByField}
        />
      </Tabs.TabPane>
      <Tabs.TabPane
        tab={t('condition')}
        key={FilterConditionType.Condition.toString()}
      >
        <CategoryConditionRelationSelector
          condition={condition}
          onConditionChange={onConditionChange}
        />
      </Tabs.TabPane>
    </StyledTabs>
  );
}
Example #23
Source File: PermsForm.tsx    From anew-server with MIT License 4 votes vote down vote up
PermsForm: React.FC<PermsFormProps> = (props) => {
    const { actionRef, modalVisible, handleChange, values } = props;
    const [menuData, setMenuData] = useState<API.MenuList[]>([]);
    const [apiData, setApiData] = useState<API.ApiList[]>([]);
    const [checkedMenu, setCheckedMenu] = useState<React.Key[]>([]);
    const [checkedApi, setCheckedApi] = useState<CheckboxValueType[]>([]);

    const onCheck = (keys: any, info: any) => {
        let allKeys = keys.checked;
        const parentKey = info.node.parent_id;
        if (allKeys.indexOf(parentKey)) {
            setCheckedMenu(allKeys);
        } else {
            allKeys = allKeys.push(parentKey);
            setCheckedMenu(allKeys);
        }
    };

    const onCheckChange = (checkedValue: CheckboxValueType[]) => {
        //console.log(a.filter(function(v){ return !(b.indexOf(v) > -1) }).concat(b.filter(function(v){ return !(a.indexOf(v) > -1)})))
        setCheckedApi(checkedValue);
    };

    useEffect(() => {
        queryMenus().then((res) => setMenuData(loopTreeItem(res.data)));
        queryApis().then((res) => setApiData(res.data));
        getRolePermsByID(values?.id).then((res) => {
            setCheckedMenu(res.data.menus_id);
            setCheckedApi(res.data.apis_id);
        });
    }, []);
    return (
        <DrawerForm
            //title="设置权限"
            visible={modalVisible}
            onVisibleChange={handleChange}
            onFinish={async () => {
                updatePermsRole({
                    menus_id: checkedMenu,
                    apis_id: checkedApi,
                }, values?.id).then((res) => {
                    if (res.code === 200 && res.status === true) {
                        message.success(res.message);
                        if (actionRef.current) {
                            actionRef.current.reload();
                        }
                    }
                })
                return true;
            }}
        >
            <h3>菜单权限</h3>
            <Divider />

            <Tree
                checkable
                checkStrictly
                style={{ width: 330 }}
                //defaultCheckedKeys={selectedKeys}
                //defaultSelectedKeys={selectedKeys}
                autoExpandParent={true}
                selectable={false}
                onCheck={onCheck}
                checkedKeys={checkedMenu}
                treeData={menuData as any}
            />

            <Divider />
            <h3>API权限</h3>
            <Divider />
            <Checkbox.Group style={{ width: '100%' }} value={checkedApi} onChange={onCheckChange}>
                {apiData.map((item, index) => {
                    return (
                        <div key={index}>
                            <h4>{item.name}</h4>
                            <Row>
                                {item.children?.map((item, index) => {
                                    return (
                                        <Col span={4} key={index}>
                                            <Checkbox value={item.id}>{item.name}</Checkbox>
                                        </Col>
                                    );
                                })}
                            </Row>
                            <Divider />
                        </div>
                    );
                })}
            </Checkbox.Group>
        </DrawerForm>
    );
}
Example #24
Source File: index.tsx    From jetlinks-ui-antd with MIT License 4 votes vote down vote up
OpcUaComponent: React.FC<Props> = props => {

    const initState: State = {
        searchParam: { pageSize: 10 },
        searchPointParam: { pageSize: 10, sorts: { field: 'property', order: 'desc' } },
        pointVisible: false,
        bindSaveVisible: false,
        pointSaveVisible: false,
        channelSaveVisible: false,
        bindDeviceVisible: false,
        currentChannel: {},
        currentBind: {},
        currentPoint: {},
        result: {},
        resultPoint: {},
        dataListNoPaing: [],
        opcId: '',
        deviceId: '',
        deviceBindId: '',
        propertyList: [],
        selectedRowKeys: [],
        device: {}
    };

    const [searchParam, setSearchParam] = useState(initState.searchParam);
    const [searchPointParam, setSearchPointParam] = useState(initState.searchPointParam);
    const [result, setResult] = useState(initState.result);
    const [resultPoint, setResultPoint] = useState(initState.resultPoint);
    const [pointVisible, setPointVisible] = useState(initState.pointVisible);
    const [channelSaveVisible, setChannelSaveVisible] = useState(initState.channelSaveVisible);
    const [bindSaveVisible, setBindSaveVisible] = useState(initState.bindSaveVisible);
    const [pointSaveVisible, setPointSaveVisible] = useState(initState.pointSaveVisible);
    const [bindDeviceVisible, setBindDeviceVisible] = useState(initState.bindDeviceVisible);
    const [currentChannel, setCurrentChannel] = useState(initState.currentChannel);
    const [currentBind, setCurrentBind] = useState(initState.currentBind);
    const [currentPoint, setCurrentPoint] = useState(initState.currentPoint);
    const [dataListNoPaing, setDataListNoPaing] = useState(initState.dataListNoPaing);
    const [opcId, setOpcId] = useState(initState.opcId);
    const [device, setDevice] = useState(initState.device);
    const [deviceId, setDeviceId] = useState(initState.deviceId);
    const [deviceBindId, setDeviceBindId] = useState(initState.deviceBindId);
    const [importVisible, setImportVisible] = useState(false);
    const [exportVisible, setExportVisible] = useState(false);
    const [treeNode, setTreeNode] = useState<any>({});
    const [spinning, setSpinning] = useState(true);
    const [properties$, setProperties$] = useState<any>();
    const [selectedRowKeys, setSelectedRowKeys] = useState([]);
    const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
    const wsCallback = useRef();

    const getListNoPaging = (id?: string) => {
        setSpinning(true);
        apis.opcUa.listNoPaging(encodeQueryParam({ //加载通道
            sorts: { field: 'name', order: 'desc' }
        })).then((res: any) => {
            if (res.status === 200) {
                let data: any[] = [];
                if (res.result.length > 0) {
                    res.result.map((item: any) => {
                        data.push({
                            key: item.id,
                            title: rendertitle(item),
                            isLeaf: false,
                            children: [],
                            id: item.id
                        })
                    })
                    setDataListNoPaing([...data]);
                    let opcUaId = id;
                    if (id) {
                        setOpcId(id);
                        opcUaId = id;
                    } else {
                        setOpcId(data[0].key);//初始化第一个
                        opcUaId = data[0].key;
                    }
                    getDeviceBindList({//获取右侧的设备列表
                        terms: {
                            opcUaId: opcUaId
                        },
                        pageSize: 10
                    });
                } else {
                    setDataListNoPaing([]);
                    setResult({});
                    setCurrentPoint({});
                }
            }
            setSpinning(false);
        })
    }

    const getDeviceBindList = (params?: any) => {
        setSpinning(true);
        setSearchParam(params);
        apis.opcUa.getDeviceBindList(encodeQueryParam(params)).then(resp => {
            if (resp.status === 200) {
                setResult(resp.result);
            }
            setSpinning(false);
        })
    }

    const getDevicePointList = (params?: any, devices?: any) => {
        setSpinning(true);
        setSearchPointParam(params);
        apis.opcUa.getDevicePointList(encodeQueryParam(params)).then(resp => {
            if (resp.status === 200) {
                setResultPoint(resp.result);
                if(devices){
                    setDevice(devices);
                    propertiesWs(params.terms.deviceId, resp.result, devices);
                }else{
                    propertiesWs(params.terms.deviceId, resp.result, device);
                }
            }
            setSpinning(false);
        })
    }

    useEffect(() => {
        getListNoPaging(); //初始化
    }, []);

    const statusMap = new Map();
    statusMap.set('在线', 'success');
    statusMap.set('离线', 'error');
    statusMap.set('未激活', 'processing');
    statusMap.set('online', 'success');
    statusMap.set('offline', 'error');
    statusMap.set('notActive', 'processing');
    statusMap.set('enabled', 'success');
    statusMap.set('disabled', 'error');
    statusMap.set('disconnected', 'processing');

    const textPointMap = new Map();
    textPointMap.set('good', '正常');
    textPointMap.set('failed', '异常');
    textPointMap.set('enable', '启用');
    textPointMap.set('disable', '禁用');

    const statusPointMap = new Map();
    statusPointMap.set('failed', 'error');
    statusPointMap.set('enable', 'processing');
    statusPointMap.set('good', 'success');
    statusPointMap.set('disable', 'warning');

    const onTableChange = (
        pagination: PaginationConfig,
        filters: any,
    ) => {
        let { terms } = searchParam;
        if (filters.state) {
            if (terms) {
                terms.state = filters.state[0];
            } else {
                terms = {
                    state: filters.state[0],
                };
            }
        }
        getDeviceBindList({
            pageIndex: Number(pagination.current) - 1,
            pageSize: pagination.pageSize,
            terms,
        });
    };

    const onTablePointChange = (
        pagination: PaginationConfig,
        filters: any
    ) => {
        let { terms, sorts } = searchPointParam;
        if (filters.state) {
            if (terms) {
                terms.state = filters.state[0];
            } else {
                terms = {
                    state: filters.state[0]
                };
            }
        }
        setSelectedRowKeys([]);
        getDevicePointList({
            pageIndex: Number(pagination.current) - 1,
            pageSize: pagination.pageSize,
            terms: searchPointParam.terms,
            sorts: sorts,
        });
    };

    const rendertitle = (item: any) => (
        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
            <div style={{ width: '100px', overflow: 'hidden', marginRight: '10px', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} onClick={() => {
                setOpcId(item.id);
                getDeviceBindList({
                    pageSize: 10,
                    terms: {
                        opcUaId: item.id
                    }
                });
                setPointVisible(false);
            }}>
                <Tooltip title={item.name}>
                    {item.name}
                </Tooltip>
            </div>
            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                <div style={{ marginRight: '10px' }} onClick={() => {
                    setCurrentChannel(item);
                    setChannelSaveVisible(true);
                }}><a>编辑</a></div>
                <div style={{ marginRight: '10px' }} onClick={() => {
                    setOpcId(item.id);
                    setBindDeviceVisible(true);
                }}><a>绑定设备</a></div>
                {item.state.value === 'disabled' ?
                    <div style={{ marginRight: '10px' }} onClick={() => {
                        setExpandedKeys([item.id])
                        setSpinning(true);
                        apis.opcUa.start(item.id).then(res => {
                            if (res.status === 200) {
                                getListNoPaging(item.id);
                                message.success('操作成功!');
                            }
                            setSpinning(false);
                        })
                    }}><a>启用</a></div> :
                    <div style={{ marginRight: '10px' }} onClick={() => {
                        setExpandedKeys([item.id])
                        setSpinning(true);
                        apis.opcUa.stop(item.id).then(res => {
                            setSpinning(false);
                            getListNoPaging(item.id);
                            message.success('操作成功!');
                        })
                    }}><a>禁用</a></div>}
                <div style={{ marginRight: '10px' }}>
                    <Popconfirm
                        placement="topRight"
                        title="确定删除吗?"
                        onConfirm={() => {
                            apis.opcUa.remove(item.id).then(res => {
                                if (res.status === 200) {
                                    getListNoPaging();
                                    setExpandedKeys([]);
                                }
                            })
                        }}
                    >
                        <a>删除</a>
                    </Popconfirm>
                </div>
            </div>
        </div>
    )
    //绑定设备
    const columns = [
        {
            title: '设备ID',
            align: 'center',
            dataIndex: 'deviceId'
        },
        {
            title: '设备名称',
            align: 'center',
            dataIndex: 'name'
        },
        {
            title: '产品名称',
            align: 'center',
            dataIndex: 'productName'
        },
        {
            title: '集群节点',
            align: 'center',
            dataIndex: 'serverId'
        },
        {
            title: '采集状态',
            align: 'center',
            dataIndex: 'state',
            render: (record: any) => record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '/',
            filters: [
                {
                    text: '禁用',
                    value: 'disabled',
                },
                {
                    text: '启用',
                    value: 'enabled',
                },
                {
                    text: '已断开',
                    value: 'disconnected',
                }
            ],
            filterMultiple: false,
        },
        {
            title: '操作',
            align: 'center',
            render: (text: string, record: any) => (
                <Fragment>
                    <a onClick={() => {
                        setBindSaveVisible(true);
                        setCurrentBind(record);
                    }}>编辑</a>
                    {record.state.value === 'disabled' ? <>
                        <Divider type="vertical" />
                        <a onClick={() => {
                            apis.opcUa.startBind(record.id).then(res => {
                                if (res.status === 200) {
                                    getDeviceBindList(searchParam);
                                    message.success('操作成功!');
                                }
                            })
                        }}>开始采集</a>
                        <Divider type="vertical" />
                        <Popconfirm title="确认删除?" onConfirm={() => {
                            apis.opcUa.removeBind(record.id).then(res => {
                                if (res.status === 200) {
                                    getDeviceBindList(searchParam);
                                    if (treeNode !== {}) {
                                        onLoadData(treeNode);
                                    }
                                    message.success('操作成功!');
                                }
                            })
                        }}>
                            <a>解绑</a>
                        </Popconfirm>
                    </> : <>
                        <Divider type="vertical" />
                        <a onClick={() => {
                            apis.opcUa.stopBind(record.id).then(res => {
                                if (res.status === 200) {
                                    getDeviceBindList(searchParam);
                                    message.success('操作成功!');
                                }
                            })
                        }}>停止采集</a>
                    </>}
                    <Divider type="vertical" />
                    <a onClick={() => {
                        setDeviceId(record.deviceId);
                        setDeviceBindId(record.id);
                        getDevicePointList({
                            pageSize: 10,
                            terms: {
                                deviceId: record.deviceId
                            },
                            sorts: searchPointParam.sorts
                        }, record);
                        setPointVisible(true);
                    }}>查看点位</a>
                </Fragment>
            ),
        }
    ];

    const columnsPoint = [
        {
            title: '名称',
            align: 'center',
            width: '120px',
            dataIndex: 'name',
            ellipsis: true,
        },
        {
            title: '设备ID',
            align: 'center',
            width: '100px',
            ellipsis: true,
            dataIndex: 'deviceId'
        },
        {
            title: 'OPC点位ID',
            align: 'center',
            width: '200px',
            ellipsis: true,
            dataIndex: 'opcPointId',
            render: (text: any) => <Tooltip title={text}>{text}</Tooltip>
        },
        {
            title: '数据模式',
            width: '100px',
            align: 'center',
            ellipsis: true,
            dataIndex: 'dataMode'
        },
        {
            title: '数据类型',
            align: 'center',
            width: '100px',
            ellipsis: true,
            dataIndex: 'dataType'
        },
        {
            title: '值',
            align: 'center',
            width: '100px',
            ellipsis: true,
            dataIndex: 'value',
            render: (text: any) => <Tooltip title={text}>{text}</Tooltip>
        },
        {
            title: '状态',
            align: 'center',
            width: '80px',
            dataIndex: 'state',
            render: (text: any) => <Badge status={statusPointMap.get(text)}
                text={text ? textPointMap.get(text) : '/'} />,
            filters: [
                {
                    text: '正常',
                    value: 'good',
                },
                {
                    text: '异常',
                    value: 'failed',
                },
                {
                    text: '启用',
                    value: 'enable',
                },
                {
                    text: '禁用',
                    value: 'disable',
                }
            ],
            filterMultiple: false,
        },
        {
            title: '说明',
            align: 'center',
            width: '80px',
            ellipsis: true,
            dataIndex: 'description'
        },
        {
            title: '操作',
            align: 'center',
            width: '200px',
            render: (text: string, record: any) => (
                <Fragment>
                    <a onClick={() => {
                        setPointSaveVisible(true);
                        setCurrentPoint(record);
                    }}>编辑</a>
                    {record.state === 'disable' ?
                        <>
                            <Divider type="vertical" />
                            <a onClick={() => {
                                startPoint([record.id])
                            }}>启动</a>
                        </> :
                        <>
                            <Divider type="vertical" />
                            <a onClick={() => {
                                stopPoint([record.id]);
                            }}>禁用</a>
                        </>
                    }
                    <Divider type="vertical" />
                    <Popconfirm
                        placement="topRight"
                        title="确定删除吗?"
                        onConfirm={() => {
                            apis.opcUa.delPoint(deviceBindId, [record.id]).then(res => {
                                if (res.status === 200) {
                                    getDevicePointList(searchPointParam);
                                }
                            })
                        }}
                    >
                        <a>删除</a>
                    </Popconfirm>
                </Fragment>
            ),
        },
    ];

    const updateTreeData = (list: any[], key: React.Key, children: any[]): any[] => {
        return list.map((node: any) => {
            if (node.key === key) {
                return {
                    ...node,
                    children,
                };
            } else if (node.children) {
                return {
                    ...node,
                    children: updateTreeData(node.children, key, children),
                };
            }
            return node;
        });
    };

    const onLoadData = (treeNode: any) => {
        const { id, isLeaf } = treeNode.props;
        return new Promise<void>(resolve => {
            if (isLeaf) {
                resolve();
                return;
            }
            apis.opcUa.getDeviceBindListNoPaging(encodeQueryParam({
                terms: {
                    opcUaId: id
                }
            })).then(resp => {
                let children1: any[] = [];
                resp.result.map((item: any) => {
                    children1.push({
                        key: item.id,
                        title: item.name,
                        isLeaf: true,
                        id: item.deviceId,
                        item: item,
                        children: []
                    })
                })
                setDataListNoPaing(origin => updateTreeData(origin, id, children1));
                resolve();
            })
        });
    }
    //每次展开时都重新加载子节点
    const onLoadChildrenData = (id: string) => {
        return new Promise<void>(resolve => {
            apis.opcUa.getDeviceBindListNoPaging(encodeQueryParam({
                terms: {
                    opcUaId: id
                }
            })).then(resp => {
                let children1: any[] = [];
                resp.result.map((item: any) => {
                    children1.push({
                        key: item.id,
                        title: item.name,
                        isLeaf: true,
                        id: item.deviceId,
                        item: item,
                        children: []
                    })
                })
                setDataListNoPaing(origin => updateTreeData(origin, id, children1));
                resolve();
            })
        });
    }

    const propertiesWs = (deviceId: string, result: any, device: any) => {
        if (properties$) {
            properties$.unsubscribe();
        }
        let points: any[] = []
        result.data.map((item: any) => {
            points.push(item.property)
        })
        let str = points.join("-");
        let propertiesWs = getWebsocket(
            // `${deviceId}-opc-ua-device-point-value`,
            // `/device/*/${deviceId}/message/property/report`,
            // {},
            // `${deviceId}-opc-ua-device-point-value-${str}`,
            // `/opc-ua-point-value/${deviceId}`,
            // {
            //     points: points
            // }
            `instance-info-property-${deviceId}-${device.productId}-${str}`,
            `/dashboard/device/${device.productId}/properties/realTime`,
            {
                deviceId: deviceId,
                properties: points,
                history: 1,
            },
        ).subscribe((resp: any) => {
            const { payload } = resp;
            let resultList = [...result.data];
            resultList.map((item: any) => {
                // if (payload.properties[item.property] !== undefined) {
                //     item.value = payload.properties[item.property].formatValue
                // }
                if (payload.value.property === item.property) {
                    item.value = payload.value.formatValue
                }
            })
            setResultPoint({
                data: [...resultList],
                pageIndex: result.pageIndex,
                pageSize: result.pageSize,
                total: result.total
            })
        },
            () => { setResultPoint(result) },
            () => { setResultPoint(result) });
        setProperties$(propertiesWs);
    };

    const rowSelection = {
        onChange: (selectedRowKeys: any) => {
            setSelectedRowKeys(selectedRowKeys);
        },
    };

    const stopPoint = (list: any[]) => {
        apis.opcUa.stopPoint(deviceBindId, [...list]).then(res => {
            if (res.status === 200) {
                getDevicePointList(searchPointParam);
            }
        })
    }

    const startPoint = (list: any[]) => {
        apis.opcUa.startPoint(deviceBindId, [...list]).then(res => {
            if (res.status === 200) {
                getDevicePointList(searchPointParam);
            }
        })
    }

    useEffect(() => {
        wsCallback.current = properties$;
    })

    useEffect(() => {
        return () => {
            let properties = wsCallback.current;
            properties && properties.unsubscribe();
        }
    }, []);

    const menu = (
        <Menu>
            <Menu.Item key="1">
                <Button
                    icon="download"
                    type="default"
                    onClick={() => {
                        setExportVisible(true);
                    }}
                >
                    批量导出设备
            </Button>
            </Menu.Item>
            <Menu.Item key="2">
                <Button
                    icon="upload"
                    onClick={() => {
                        setImportVisible(true);
                    }}
                >
                    批量导入设备
            </Button>
            </Menu.Item>
            <Menu.Item key="5">
                <Button icon="check-circle" type="danger" onClick={() => {
                    Modal.confirm({
                        title: `确认全部开始采集`,
                        okText: '确定',
                        okType: 'primary',
                        cancelText: '取消',
                        onOk() {
                            apis.opcUa.startAllDevice(opcId).then(res => {
                                if (res.status === 200) {
                                    message.success('操作成功!');
                                    getDeviceBindList({
                                        terms: {
                                            opcUaId: opcId
                                        },
                                        pageSize: 10
                                    });
                                }
                            })
                        },
                    });
                }}>全部开始采集</Button>
            </Menu.Item>
        </Menu>
    );

    return (
        <Spin spinning={spinning}>
            <PageHeaderWrapper title="OPC UA">
                <Card bordered={false}>
                    <div className={style.box}>
                        <Card style={{ width: '350px' }}>
                            <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-start', margin: '0px 10px 20px 0px' }}>
                                <Button style={{ width: '100%' }} icon="plus" type="dashed" onClick={() => {
                                    setChannelSaveVisible(true);
                                    setCurrentChannel({});
                                }}>新增</Button>
                            </div>
                            <div style={{ width: '320px', height: '650px', overflowY: 'scroll' }}>
                                <Tree
                                    showIcon
                                    treeData={dataListNoPaing}
                                    // loadData={onLoadData}
                                    expandedKeys={expandedKeys}
                                    onExpand={(expandedKeys, { expanded }) => { //只展开一个
                                        if (expanded && expandedKeys.length > 0) {
                                            let keys = expandedKeys[expandedKeys.length - 1];
                                            setExpandedKeys([keys])
                                            onLoadChildrenData(keys)
                                        } else {
                                            setExpandedKeys([])
                                        }
                                    }}
                                    onSelect={(key, e) => {
                                        if (key.length > 0) {
                                            setTreeNode(e.node);
                                            const { eventKey, isLeaf, id } = e.node.props;
                                            if (isLeaf) {//选择孩子节点时的操作
                                                setDeviceId(id);
                                                setDeviceBindId(eventKey || key[0]);
                                                getDevicePointList({
                                                    pageSize: 10,
                                                    terms: {
                                                        deviceId: id
                                                    },
                                                    sorts: searchPointParam.sorts
                                                }, e.node.props.item);
                                                setPointVisible(true);
                                            }
                                        }
                                    }}
                                />
                            </div>
                        </Card>
                        <Card style={{ width: 'calc(100% - 360px)' }}>
                            <div className={styles.tableList}>
                                <div className={styles.StandardTable}>
                                    {pointVisible ?
                                        <>
                                            <div style={{ width: '100%' }}>
                                                <SearchForm
                                                    search={(params: any) => {
                                                        getDevicePointList({ terms: { ...params, deviceId: deviceId }, pageSize: 10, sorts: searchPointParam.sorts });
                                                    }}
                                                    formItems={[
                                                        {
                                                            label: 'OPC点位ID',
                                                            key: 'opcPointId$LIKE',
                                                            type: 'string'
                                                        },
                                                        {
                                                            label: '点位名称',
                                                            key: 'name$LIKE',
                                                            type: 'string'
                                                        }
                                                    ]}
                                                />
                                            </div>
                                            <div style={{ display: 'flex', marginBottom: '20px', width: '100%' }}>
                                                <div style={{ marginRight: '10px' }}><Button icon="plus" type="primary" onClick={() => {
                                                    setPointSaveVisible(true);
                                                    setCurrentPoint({});
                                                }}>新增</Button></div>
                                                {selectedRowKeys.length > 0 &&
                                                    <>
                                                        <div style={{ marginRight: '10px' }}>
                                                            <Button
                                                                icon="check-circle"
                                                                type="default"
                                                                onClick={() => {
                                                                    startPoint(selectedRowKeys);
                                                                }}
                                                            >
                                                                批量启动点位
                                                            </Button>
                                                        </div>
                                                        <div style={{ marginRight: '10px' }}>
                                                            <Button
                                                                icon="stop"
                                                                type="danger"
                                                                ghost
                                                                onClick={() => {
                                                                    stopPoint(selectedRowKeys);
                                                                }}
                                                            >
                                                                批量禁用点位
                                                             </Button>
                                                        </div>
                                                        <div style={{ marginRight: '10px' }}>
                                                            <Button
                                                                icon="check-circle"
                                                                type="danger"
                                                                onClick={() => {
                                                                    apis.opcUa.delPoint(deviceBindId, selectedRowKeys).then(res => {
                                                                        if (res.status === 200) {
                                                                            getDevicePointList(searchPointParam);
                                                                        }
                                                                    })
                                                                }}
                                                            >
                                                                批量删除点位
                                                            </Button>
                                                        </div>
                                                    </>
                                                }
                                            </div>
                                            <Table
                                                loading={props.loading}
                                                dataSource={(resultPoint || {}).data}
                                                columns={columnsPoint}
                                                rowKey="id"
                                                onChange={onTablePointChange}
                                                rowSelection={{
                                                    type: 'checkbox',
                                                    ...rowSelection,
                                                    selectedRowKeys: selectedRowKeys
                                                }}
                                                pagination={{
                                                    current: resultPoint.pageIndex + 1,
                                                    total: resultPoint.total,
                                                    pageSize: resultPoint.pageSize
                                                }}
                                            />
                                        </> :
                                        <>
                                            <div style={{ display: 'flex', marginBottom: '20px', width: '100%', flexWrap: 'wrap' }}>
                                                <div style={{ width: '100%' }}>
                                                    <SearchForm
                                                        search={(params: any) => {
                                                            getDeviceBindList({ terms: { ...params, opcUaId: opcId }, pageSize: 10 });
                                                        }}
                                                        formItems={[
                                                            {
                                                                label: '设备ID',
                                                                key: 'deviceId$LIKE',
                                                                type: 'string'
                                                            },
                                                            // {
                                                            //     label: '设备名称',
                                                            //     key: 'name$LIKE',
                                                            //     type: 'string'
                                                            // }
                                                        ]}
                                                    />
                                                </div>
                                                <div style={{ marginRight: '20px' }}><Button type="primary" icon="plus" onClick={() => {
                                                    setBindSaveVisible(true);
                                                    setCurrentBind({});
                                                }}>新增设备</Button></div>
                                                <Dropdown overlay={menu}>
                                                    <Button icon="menu">
                                                        其他批量操作
                                                        <Icon type="down" />
                                                    </Button>
                                                </Dropdown>
                                            </div>
                                            <Table
                                                loading={props.loading}
                                                dataSource={(result || {}).data}
                                                columns={columns}
                                                rowKey="id"
                                                onChange={onTableChange}
                                                pagination={{
                                                    current: result.pageIndex + 1,
                                                    total: result.total,
                                                    pageSize: result.pageSize
                                                }}
                                            />
                                        </>}
                                </div>
                            </div>
                        </Card>
                    </div>
                </Card>
                {channelSaveVisible && <ChannelSave data={currentChannel} close={() => {
                    setChannelSaveVisible(false);
                }} save={(data: any) => {
                    setChannelSaveVisible(false);
                    if (currentChannel.id) {
                        apis.opcUa.update(data).then(res => {
                            if (res.status === 200) {
                                message.success('保存成功!');
                                getListNoPaging(data.id);
                            }
                        })
                    } else {
                        apis.opcUa.save(data).then(res => {
                            if (res.status === 200) {
                                message.success('保存成功!');
                                getListNoPaging(data.id);
                            }
                        })
                    }
                }} />}
                {pointSaveVisible && <PointSave data={currentPoint}
                    deviceId={deviceId}
                    opcUaId={opcId}
                    close={() => {
                        setPointSaveVisible(false);
                    }} save={(data: any) => {
                        setPointSaveVisible(false);
                        apis.opcUa.savePoint(data).then(res => {
                            if (res.status === 200) {
                                message.success('保存成功!');
                                getDevicePointList({
                                    pageSize: 10,
                                    terms: {
                                        deviceId: deviceId
                                    },
                                    sorts: searchPointParam.sorts
                                });
                            }
                        })
                    }} />}
                {bindSaveVisible && <BindSave data={currentBind} close={() => {
                    setBindSaveVisible(false);
                }} opcId={opcId} save={() => {
                    setBindSaveVisible(false);
                    if (treeNode !== {}) {
                        onLoadData(treeNode);
                    }
                    getDeviceBindList(searchParam);
                }} />}
                {importVisible && (
                    <Import
                        opcId={opcId}
                        close={() => {
                            setImportVisible(false);
                            if (treeNode !== {}) {
                                onLoadData(treeNode);
                            }
                            getDeviceBindList({
                                pageSize: 10,
                                terms: {
                                    opcUaId: opcId
                                }
                            });
                        }}
                    />
                )}
                {exportVisible && (
                    <Export
                        searchParam={searchParam}
                        close={() => {
                            setExportVisible(false);
                        }}
                    />
                )}
                {bindDeviceVisible && <BindDevice opcId={opcId}
                    close={() => {
                        setBindDeviceVisible(false);
                        if (treeNode !== {}) {
                            onLoadData(treeNode);
                        }
                        getDeviceBindList({
                            pageSize: 10,
                            terms: {
                                opcUaId: opcId
                            }
                        });
                    }} />}
            </PageHeaderWrapper>
        </Spin>
    );
}
Example #25
Source File: index.tsx    From jetlinks-ui-antd with MIT License 4 votes vote down vote up
Reveal: React.FC<Props> = props => {
  const service = new Service('media/gateway');

  const [treeData, setTreeData] = useState<DataNode>();
  const [players, setPlayers] = useState([{
    url: "", //http://mirror.aarnet.edu.au/pub/TED-talks/911Mothers_2010W-480p.mp4
    bLoading: false,
    timer: 0,
    bCloseShow: false,
    closeTimer: 0,
    serial: "",
    code: "",
    protocol: "",
    poster: "",
    deviceId: '',
    channelId: ''
  }]);
  const [playing, setPlaying] = useState(true);
  const [setting, setSetting] = useState(0);
  const [deviceId, setDeviceId] = useState('');
  const [channelId, setChannelId] = useState('');
  const [playUp, setPlayUp] = useState<boolean>(true);
  const [playDown, setPlayDown] = useState<boolean>(true);
  const [playLeft, setPlayLeft] = useState<boolean>(true);
  const [playCenter, setPlayCenter] = useState<boolean>(false);
  const [playRight, setPlayRight] = useState<boolean>(true);
  const [playIn, setPlayIn] = useState<boolean>(true);
  const [playOut, setPlayOut] = useState<boolean>(true);
  const [playerActive, setPlayerActive] = useState(0);
  const playerBtnGroup = [{ num: 1, name: '单屏' }, { num: 4, name: '四分屏' }, { num: 9, name: '九分屏' }];

  useEffect(() => {
    setPlaying(true);
    //获取信令服务
    let datalist: DataNode[] = [];
    service.getProduct(encodeQueryParam({ terms: location?.query?.terms })).subscribe(
      (result) => {
        if (result.length > 0) {
          result.map((i: any) => {
            service.groupDevice(encodeQueryParam({
              terms: {
                productId: i.productId,
              }
            })).subscribe((data) => {
              if (data.length > 0) {
                data.map((item: any) => {
                  datalist.push({
                    title: item.name,
                    key: item.id,
                    isLeaf: false,
                    icon: <ApartmentOutlined />,
                    channelId: '',
                    deviceId: '',
                    children: []
                  })
                })
              }
              setTreeData(datalist)
            })
          })
        }
      },
      () => {
      });
    document.addEventListener('fullscreenchange', function () {
      if (document.fullscreenElement) {
        setSetting(10);
      } else {
        setSetting(0);
      }
    }, false);
  }, []);
  const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): any[] => {
    return list.map((node: any) => {
      if (node.key === key) {
        return {
          ...node,
          children,
        };
      } else if (node.children) {
        return {
          ...node,
          children: updateTreeData(node.children, key, children),
        };
      }
      return node;
    });
  };
  const setPlayerLength = (playerLength: number) => {
    let data: any = [];
    for (let i = 0; i < playerLength; i++) {
      data.push({
        url: "",
        bLoading: false,
        timer: 0,
        bCloseShow: false,
        closeTimer: 0,
        serial: "",
        code: "",
        protocol: "",
        poster: "",
        deviceId: '',
        channelId: ''
      })
    }
    setSetting(0);
    setPlayers(data);
  };
  const loadChannel = (node: any) => {
    const { eventKey, isLeaf } = node.props
    return new Promise<void>(resolve => {
      if (isLeaf) {
        resolve();
        return;
      }
      let children1: DataNode[] = []
      service.getChannel(encodeQueryParam({
        terms: {
          deviceId: eventKey
        }
      })).subscribe((res) => {
        if (res.length > 0) {
          res.map((it: any) => {
            children1.push({
              title: it.name,
              key: it.id,
              isLeaf: true,
              icon: it.status.value === 'online' ? <VideoCameraTwoTone twoToneColor="#52c41a" /> : <VideoCameraOutlined />,
              channelId: it.channelId,
              deviceId: it.deviceId,
              children: []
            })
          });
          setTreeData(origin => updateTreeData(origin, eventKey, children1));
          resolve();
        }
      })
    })
  };
  const playVideo = (e) => {
    const { deviceId, channelId, isLeaf } = e.node.props;
    setDeviceId(deviceId);
    setChannelId(channelId);
    if (isLeaf) {
      service.getPlay(deviceId, channelId).subscribe(res => {
        let data = players || [];
        data.forEach((item, index) => {
          if (index === setting) {
            item.url = getPlayer(res).url;
            item.protocol = getPlayer(res).protocol;
            item.deviceId = deviceId;
            item.channelId = channelId
          }
        });
        let i = 0;
        if (players.length - 1 > setting) {
          i = setting + 1;
        } else if (players.length - 1 === setting) {
          i = 0
        }
        setSetting(i);
        setPlayers([...data])
      })
    }
  };

  const getPlayer = (res: any) => {
    if (res.mp4) {
      return { url: res.mp4, protocol: 'mp4' }
    } else if (res.flv) {
      return { url: res.flv, protocol: 'flv' }
    } else if (res.hls) {
      return { url: res.hls, protocol: 'hls' }
    } else if (res.rtmp) {
      return { url: res.rtmp, protocol: 'rtmp' }
    } else if (res.rtsp) {
      return { url: res.rtsp, protocol: 'rtsp' }
    } else if (res.rtc) {
      return { url: res.rtc, protocol: 'rtc' }
    } else {
      return { url: '', protocol: '' }
    }
  };

  const controlStart = (deviceId: string, channelId: string, direct: string) => {
    if (playing && deviceId !== '' && channelId !== '' && deviceId !== undefined && channelId !== undefined) {
      service.getControlStart(deviceId, channelId, direct, 90).subscribe(() => {
      })
    }
  };
  const controlStop = (deviceId: string, channelId: string) => {
    if (playing && deviceId !== '' && channelId !== '' && deviceId !== undefined && channelId !== undefined) {
      service.getControlStop(deviceId, channelId).subscribe(() => {
      })
    }
  };

  const fullScreen = () => {
    let dom = document.getElementById('video_show');
    if (dom?.requestFullscreen) {
      dom.requestFullscreen();
    }
  };

  //刷新
  const refresh = (deviceId: string, channelId: string) => {
    //关闭流
    service.getStop(deviceId, channelId).subscribe(() => {
      //开启流
      service.getPlay(deviceId, channelId).subscribe(res => {
        let data = players || [];
        data.forEach((item, index) => {
          if (index === setting) {
            item.url = getPlayer(res).url;
            item.protocol = getPlayer(res).protocol;
            item.deviceId = deviceId;
            item.channelId = channelId
          }
        });
        setPlayers([...data])
      })
    });
  };

  return (
    <PageHeaderWrapper title="分屏展示">
      <Card bordered={false} style={{ marginBottom: 16 }}>
        <div className={styles.box}>
          <div className={styles.device_tree}>
            <Tabs defaultActiveKey="1">
              <Tabs.TabPane tab="设备树" key="1">
                <div className={styles.device_tree_tab}>
                  <Tree
                    showIcon
                    defaultExpandAll
                    switcherIcon={<DownOutlined />}
                    treeData={treeData}
                    loadData={loadChannel}
                    onSelect={(key, e) => { playVideo(e) }}
                  />
                </div>
              </Tabs.TabPane>
            </Tabs>
          </div>
          <div className={styles.player}>
            <div className={styles.top}>
              <div className={styles.btn_box}>
                {playerBtnGroup.length > 0 && playerBtnGroup.map((item, index) => (
                  <div key={index} className={styles.btn} onClick={() => { setPlayerActive(index); setPlayerLength(item.num); }} style={index === playerActive ? { backgroundColor: '#404d59', color: '#fff' } : {}}>{item.name}</div>
                ))}
                <div className={styles.btn} onClick={() => { fullScreen() }}>全屏</div>
              </div>
            </div>
            <div className={styles.player_box}>
              <div className={styles.player_left} id="video_show">
                {
                  players.length > 0 && players.map((item: any, index: number) => (
                    <div onClick={() => { if (!document.fullscreenElement) { setSetting(index); setDeviceId(item.deviceId); setChannelId(item.channelId); } }} className={styles.video} key={index} style={players.length === 1 ? { border: setting === index ? "1px solid red" : null, width: 'calc(100% - 10px)' } : players.length === 9 ? { border: setting === index ? "1px solid red" : null, width: "calc((100% -  30px) / 3)" } : { width: "calc((100% -  20px) / 2)", border: setting === index ? "1px solid red" : null }}>
                      <live-player loading={item.bLoading} muted stretch protocol={item.protocol} element-loading-text="加载中..." element-loading-background="#000" autoplay live video-url={item.url}></live-player>
                      {item.deviceId !== '' && item.channelId !== '' && <div className={styles.video_lose} onClick={() => { refresh(item.deviceId, item.channelId) }}>刷新</div>}
                    </div>
                  ))
                }
              </div>
              <div className={styles.player_right}>
                <div className={styles.ptz_block}>
                  <div className={styles.ptz_up} title="上" onMouseDown={() => { controlStart(deviceId, channelId, 'UP'); setPlayUp(false); }} onMouseUp={() => { controlStop(deviceId, channelId); setPlayUp(true); }}>
                    {playing && playUp ? <img src="/img/up.svg" width="30px" /> : <img src="/img/up_1.svg" width="30px" />}
                  </div>
                  <div className={styles.ptz_left} title="左" onMouseDown={() => { controlStart(deviceId, channelId, 'LEFT'); setPlayLeft(false); }} onMouseUp={() => { controlStop(deviceId, channelId); setPlayLeft(true); }}>
                    {playing && playLeft ? <img src="/img/left.svg" width="30px" /> : <img src="/img/left_1.svg" width="30px" />}
                  </div>
                  <div className={styles.ptz_center} title="云控制台">
                    {playing && playCenter ? <img src="/img/audio.svg" width="30px" /> : <img src="/img/audio_1.svg" width="30px" />}
                  </div>
                  <div className={styles.ptz_right} title="右" onMouseDown={() => { controlStart(deviceId, channelId, 'RIGHT'); setPlayRight(false); }} onMouseUp={() => { controlStop(deviceId, channelId); setPlayRight(true); }}>
                    {playing && playRight ? <img src="/img/right.svg" width="30px" /> : <img src="/img/right_1.svg" width="30px" />}
                  </div>
                  <div className={styles.ptz_down} title="下" onMouseDown={() => { controlStart(deviceId, channelId, 'DOWN'); setPlayDown(false); }} onMouseUp={() => { controlStop(deviceId, channelId); setPlayDown(true); }}>
                    {playing && playDown ? <img src="/img/down.svg" width="30px" /> : <img src="/img/down_1.svg" width="30px" />}
                  </div>
                  <div className={styles.ptz_zoomin} title="放大" onMouseDown={() => { controlStart(deviceId, channelId, 'ZOOM_IN'); setPlayIn(false); }} onMouseUp={() => { controlStop(deviceId, channelId); setPlayIn(true); }}>
                    {playing && playIn ? <img src="/img/add.svg" width="30px" /> : <img src="/img/add_1.svg" width="30px" />}
                  </div>
                  <div className={styles.ptz_zoomout} title="缩小" onMouseDown={() => { controlStart(deviceId, channelId, 'ZOOM_OUT'); setPlayOut(false); }} onMouseUp={() => { controlStop(deviceId, channelId); setPlayOut(true); }}>
                    {playing && playOut ? <img src="/img/sub.svg" width="30px" /> : <img src="/img/sub_1.svg" width="30px" />}
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </Card>
    </PageHeaderWrapper>
  )
}
Example #26
Source File: index.tsx    From jetlinks-ui-antd with MIT License 4 votes vote down vote up
QuickInsertComponent: React.FC<Props> = (props) => {

    const service = new Service('product/properties-edit');
    const [metaDataList, setMetaDataList] = useState<any[]>([]);
    const [metaList, setMetaList] = useState<any[]>([]);
    const [enterKey, setEnterKey] = useState<string>('property');
    const [data, setData] = useState<any>({});
    const [loading, setLoading] = useState<boolean>(false);
    const [propertyVisible, setPropertyVisible] = useState(false);
    const [property, setProperty] = useState({});

    const onSearch = (value: any) => {
        if(!value){
            setMetaDataList([...metaList])
            return;
        }
        let data: any[] = [];
        //只要满足就返回这个节点及其父节点
        metaList.map((item: any) => {
            let list1: any[] = []
            let list3: any[] = []
            if (item.children && item.children.length > 0) {
                item.children.map((child1: any) => {
                    let list2: any[] = []
                    if (child1.children && child1.children.length > 0) {
                        child1.children.map((child2: any) => {
                            if (child2.name.includes(value) && child2.children.length === 0){
                                list2.push(child2)
                            }
                        })
                    }else if (child1.name.includes(value) && child1.children.length === 0){
                        list1.push(child1)
                    }
                    if(list2.length > 0){
                        list3.push({...child1, children: [...list2]})
                    }
                })
            }
            if(list3.length > 0){
                data.push({...item, children: [...list3]})
            }
            if(list1.length > 0){
                data.push({...item, children: [...list1]})
            }
        })
        setMetaDataList([...data])
    }

    useEffect(() => {
        let children: any[] = [];
        if (props.metaDataList.length > 0) {
            props.metaDataList.map(item => {
                children.push({
                    id: item.id,
                    name: item.name,
                    children: [],
                    description: `### ${item.name}  
                    \n 数据类型:${item.valueType?.type}  
                    \n 是否只读:${item.expands?.readOnly}  
                    \n 可写数值范围:---`
                })
            })
        }
        let metaData = {
            id: 'property',
            name: '属性',
            children: [...children],
            description: '',
            code: ''
        }
        setLoading(true);
        service.getDescriptionList({}).subscribe(
            res => {
                if (res.status === 200) {
                    setMetaDataList([metaData, ...res.result]);
                    setMetaList([metaData, ...res.result]);//获取全部数据
                }
                setLoading(false);
            }
        )
    }, []);

    const rendertitle = (item: any) => (
        <div style={{ display: 'flex', justifyContent: 'space-between', width: '220px' }} onMouseEnter={() => {
            setEnterKey(item.id);
        }}>
            <div>{item.name}</div>
            {item.children.length === 0 && enterKey === item.id && (<div onClick={() => {
                if(item.code === undefined || item.code===''){
                    setPropertyVisible(true);
                    setProperty(item);
                }else{
                    props.insertContent(item.code)
                }
            }}><a>添加</a></div>)}
        </div>
    )

    const getView = (view: any) => {
        return (
            <TreeNode title={rendertitle(view)} value={view.id} key={view.id}>
                {
                    view.children && view.children.length > 0 ? view.children.map((v: any) => {
                        return getView(v);
                    }) : ''
                }
            </TreeNode>
        )
    };

    const getDataItem = (dataList: any, value: string) => {
        dataList.map((item: any) => {
            if (item.id === value) {
                setData(item)
            }
            if (item.children && item.children.length > 0) {
                getDataItem(item.children, value);
            }
        })
    }; //metaList

    return (
        <Spin spinning={loading}>
            <div className={styles.box}>
                <Search placeholder="搜索关键字" allowClear onSearch={onSearch} style={{ width: '100%' }} />
                <div className={styles.treeBox}>
                    <Tree
                        defaultExpandedKeys={['property']}
                        onSelect={(selectedKeys) => {
                            getDataItem(metaDataList, selectedKeys[0])
                        }}
                    >
                        {
                            metaDataList.map(item => {
                                return getView(item);
                            })
                        }
                    </Tree>
                </div>
                <div className={styles.explain}>
                    <ReactMarkdown>{data.description}</ReactMarkdown>
                </div>
            </div>
            {propertyVisible && <PropertyComponent data={property} close={() => {
                setPropertyVisible(false);
            }} ok={(data: any) => {
                props.insertContent(data);
                setPropertyVisible(false);
            }}/>}
        </Spin>
    );
}
Example #27
Source File: tree.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
TreeCategory = ({
  title,
  titleOperation,
  initTreeData = [],
  treeProps,
  currentKey,
  actions,
  iconMap,
  loading,
  effects,
  searchGroup,
  onSelectNode = noop,
  customNode,
  customNodeClassName,
}: IProps) => {
  const { loadData, getAncestors, copyNode, moveNode, createNode, updateNode, deleteNode, fuzzySearch } = effects;

  const [{ treeData, expandedKeys, isEditing, rootKey, cuttingNodeKey, copyingNodeKey, filterOptions }, updater] =
    useUpdate({
      treeData: map(initTreeData || [], (data) => ({
        ...data,
        icon: getIcon(data, iconMap),
        optionProps: {
          popover: {
            trigger: 'hover',
            getPopupContainer: () => document.body,
            onVisibleChange: (visible: boolean) =>
              onPopMenuVisibleChange(data.isLeaf ? `leaf-${data.key}` : data.key, visible),
          },
        },
      })) as TreeNode[],
      expandedKeys: [] as string[],
      isEditing: false,
      rootKey: get(initTreeData, '0.key') || '0',
      cuttingNodeKey: null as null | string,
      copyingNodeKey: null as null | string,
      filterOptions: { folderGroup: [], fileGroup: [] } as {
        folderGroup: Array<{ label: string; value: string }>;
        fileGroup: Array<{ label: string; value: string }>;
      },
    });

  const latestTreeData = useLatest(treeData);

  const getClassName = (node: TreeNode) => {
    const { isLeaf } = node;
    return isLeaf ? (typeof customNodeClassName === 'function' ? customNodeClassName(node) : customNodeClassName) : '';
  };

  const onPopMenuVisibleChange = (key: string, visible: boolean) => {
    const dataCp = cloneDeep(latestTreeData.current);
    const node = findTargetNode(key, dataCp);
    set(node!, ['optionProps', 'popover', 'visible'], visible);
    updater.treeData(dataCp);
  };

  // 更新一个父节点的儿子节点
  const appendChildren = (parentKey: string, children: TreeNode[]) => {
    const dataCp = cloneDeep(latestTreeData.current);
    const parentNode = findTargetNode(parentKey, dataCp);
    if (parentNode) {
      set(
        parentNode,
        'children',
        map(children, (child) => {
          const { isLeaf, key, title: cTitle, ...rest } = child;
          let originChild = null; // 如果能找到此儿子本身存在,那么此儿子的儿子要保留
          if (parentNode.children && findIndex(parentNode.children, { key }) > -1) {
            originChild = find(parentNode.children, { key });
          }
          return {
            ...rest,
            title: customNode ? customNode(child) : cTitle,
            titleAlias: typeof cTitle === 'string' ? cTitle : undefined,
            isLeaf,
            key: isLeaf ? `leaf-${key}` : key,
            icon: getIcon({ isLeaf, type: rest.type, iconType: rest.iconType }, iconMap),
            parentKey,
            disabled: isEditing,
            children: originChild?.children,
            className: getClassName(child),
            optionProps: {
              popover: {
                trigger: 'hover',
                getPopupContainer: () => document.body,
                onVisibleChange: (visible: boolean) => onPopMenuVisibleChange(isLeaf ? `leaf-${key}` : key, visible),
              },
            },
          };
        }),
      );
      updater.treeData(dataCp);
    }
  };

  // 展开节点,调用接口
  const onLoadTreeData = async (nodeKey: string) => {
    const categories = await loadData({ pinode: nodeKey });
    appendChildren(nodeKey, categories);
  };

  useMount(async () => {
    if (currentKey) {
      // 刷新页面或首次加载时有具体文件id存在,需要自动展开树
      let key = currentKey;
      if (currentKey.startsWith('leaf-')) {
        key = currentKey.slice(5);
      }
      const ancestors = await getAncestors({ inode: key });
      const keys = map(ancestors, 'key');
      updater.expandedKeys(keys);
    } else if (initTreeData && initTreeData.length === 1) {
      // 否则自动展开第一层
      const categories = await loadData({ pinode: rootKey });
      appendChildren(rootKey, categories);
      updater.expandedKeys([rootKey]);
    }
  });

  const onExpand = (keys: string[]) => {
    updater.expandedKeys(keys);
  };

  const onClickNode = (keys: string[], info: { node: { props: AntTreeNodeProps } }) => {
    const isLeaf = !!info.node.props.isLeaf;
    onSelectNode({ inode: isLeaf ? keys[0].slice(5) : keys[0], isLeaf });
  };

  React.useEffect(() => {
    updater.treeData((oldTreeData: TreeNode[]) => {
      const dataCp = cloneDeep(oldTreeData);
      walkTree(dataCp, { disabled: isEditing }, EDIT_KEY);
      return dataCp;
    });
  }, [isEditing, updater]);

  function createTreeNode(nodeKey: string, isCreateFolder: boolean) {
    const dataCp = cloneDeep(treeData);
    const currentNode = findTargetNode(nodeKey, dataCp)!;
    updater.isEditing(true);
    const onEditFolder = async ({ name }: { name: string }) => {
      await createNode!({ name, pinode: nodeKey, type: isCreateFolder ? 'd' : 'f' });
      onLoadTreeData(nodeKey);
      onHide();
      updater.isEditing(false);
    };
    const onHide = () => {
      onLoadTreeData(nodeKey);
      updater.isEditing(false);
    };
    currentNode.children = [
      {
        key: EDIT_KEY,
        titleAlias: EDIT_KEY,
        parentKey: nodeKey,
        isLeaf: !isCreateFolder,
        icon: getIcon({ isLeaf: !isCreateFolder }, iconMap),
        disableAction: true,
        title: <EditCategory contentOnly onSubmit={onEditFolder} onHide={onHide} />,
      },
      ...(currentNode.children || []),
    ];
    !expandedKeys.includes(nodeKey) && updater.expandedKeys(expandedKeys.concat(nodeKey));
    updater.treeData(dataCp);
  }

  // 添加子文件夹(提交在组件内)
  const renameTreeNode = (nodeKey: string, isRenameFolder: boolean) => {
    const dataCp = cloneDeep(treeData);
    const currentNode = findTargetNode(nodeKey, dataCp)!;
    updater.isEditing(true);
    const restoreNode = () => {
      currentNode.key = nodeKey; // 利用闭包恢复key
      currentNode.disableAction = false;
      updater.treeData(dataCp);
      updater.isEditing(false);
    };
    const onEditFolder = async ({ name }: { name: string }) => {
      const updatedNode = await updateNode!({ name, inode: isRenameFolder ? nodeKey : nodeKey.slice(5) });
      currentNode.title = customNode ? customNode(updatedNode) : updatedNode.title;
      currentNode.titleAlias = name;
      restoreNode();
    };
    const onHide = () => {
      currentNode.title = currentNode.titleAlias; // 恢复被edit组件替代了的name
      restoreNode();
    };
    currentNode.key = EDIT_KEY;
    currentNode.disableAction = true;
    currentNode.title = (
      <EditCategory contentOnly defaultName={currentNode.title as string} onSubmit={onEditFolder} onHide={onHide} />
    );
    updater.treeData(dataCp);
  };

  const handleMoveNode = async (nodeKey: string, parentKey: string) => {
    const targetNode = findTargetNode(nodeKey, treeData)!;
    await moveNode!({
      inode: targetNode.isLeaf ? nodeKey.slice(5) : nodeKey,
      pinode: parentKey,
      isLeaf: targetNode!.isLeaf,
    });
    if (cuttingNodeKey === nodeKey) {
      updater.cuttingNodeKey(null);
    }
    await onLoadTreeData(targetNode.parentKey!); // 重新刷被剪切的父文件夹
    await onLoadTreeData(parentKey); // 重新刷被粘贴的父文件夹
    if (!targetNode.isLeaf && !!targetNode.children) {
      // 如果剪切的是文件夹,那么可能这个文件夹已经被展开了,那么刷新父文件夹之后children就丢了。所有手动粘贴一下
      const dataCp = cloneDeep(latestTreeData.current);
      const nodeInNewParent = findTargetNode(nodeKey, dataCp);
      if (nodeInNewParent) {
        nodeInNewParent.children = targetNode.children;
        updater.treeData(dataCp);
      }
    }
    if (!expandedKeys.includes(parentKey)) {
      updater.expandedKeys(expandedKeys.concat(parentKey));
    }
  };

  const onPaste = async (nodeKey: string) => {
    if (cuttingNodeKey) {
      await handleMoveNode(cuttingNodeKey!, nodeKey);
    } else if (copyingNodeKey) {
      const dataCp = cloneDeep(treeData);
      const targetNode = findTargetNode(copyingNodeKey, dataCp)!;
      copyNode &&
        (await copyNode({ inode: targetNode.isLeaf ? copyingNodeKey.slice(5) : copyingNodeKey, pinode: nodeKey }));
      targetNode.className = getClassName(targetNode);
      updater.copyingNodeKey(null);
      updater.treeData(dataCp);
      await onLoadTreeData(nodeKey);
      if (!expandedKeys.includes(nodeKey)) {
        updater.expandedKeys(expandedKeys.concat(nodeKey));
      }
    }
  };

  const onDelete = async (nodeKey: string) => {
    const currentNode = findTargetNode(nodeKey, treeData)!;
    // 检查当前删除的节点是不是currentKey的父级
    await deleteNode!(
      { inode: currentNode.isLeaf ? nodeKey.slice(5) : nodeKey },
      currentNode.isLeaf ? currentKey === nodeKey : isAncestor(treeData, currentKey, nodeKey),
    );
    onLoadTreeData(currentNode.parentKey!);
    if (nodeKey === copyingNodeKey) {
      updater.copyingNodeKey(null);
    } else if (nodeKey === cuttingNodeKey) {
      updater.cuttingNodeKey(null);
    }
  };

  const onCopyOrCut = (nodeKey: string, isCut: boolean) => {
    const dataCp = cloneDeep(treeData);
    if (copyingNodeKey) {
      // 先把上一个剪切的点取消
      const originalNode = findTargetNode(copyingNodeKey, dataCp)!;
      originalNode.className = getClassName(originalNode);
    }
    if (cuttingNodeKey) {
      const originalNode = findTargetNode(cuttingNodeKey, dataCp)!;
      originalNode.className = getClassName(originalNode);
    }
    const node = findTargetNode(nodeKey, dataCp)!;
    node.className = `border-dashed ${getClassName(node)}`;
    if (isCut) {
      updater.cuttingNodeKey(nodeKey); // 复制和剪切互斥关系
      updater.copyingNodeKey(null);
    } else {
      updater.copyingNodeKey(nodeKey);
      updater.cuttingNodeKey(null);
    }
    updater.treeData(dataCp);
  };

  const cancelCutCopyAction = (isCut: boolean) => ({
    node: isCut ? i18n.t('common:cancel cut') : i18n.t('common:cancel copy'),
    func: (key: string) => {
      const dataCp = cloneDeep(treeData);
      const node = findTargetNode(key, dataCp)!;
      node.className = getClassName(node);
      updater.cuttingNodeKey(null);
      updater.copyingNodeKey(null);
      updater.treeData(dataCp);
    },
  });

  const presetMap: {
    [p: string]: {
      node?: string | JSX.Element;
      func: (key: string) => void;
      condition?: (node: TreeNode) => boolean | IAction;
      hookFn?: (nodeKey: string, isCreate: boolean) => Promise<void>;
    } | null;
  } = {
    [NEW_FOLDER]: createNode
      ? {
          func: (key: string) => createTreeNode(key, true),
          condition: (node) => node.key !== copyingNodeKey && node.key !== cuttingNodeKey, // 本身是被复制或剪切中的节点不能创建新节点
        }
      : null,
    [RENAME_FOLDER]: updateNode
      ? {
          func: (key: string) => renameTreeNode(key, true),
          condition: (node) => node.key !== copyingNodeKey && node.key !== cuttingNodeKey, // 本身是被复制或剪切中的节点不能重命名
        }
      : null,
    [DELETE]: deleteNode
      ? {
          node: (
            <Popover trigger="click" content={i18n.t('dop:confirm to delete?')} onCancel={(e) => e.stopPropagation()}>
              <div
                onClick={(e) => {
                  e.stopPropagation();
                }}
              >
                {i18n.t('Delete')}
              </div>
            </Popover>
          ),
          func: async (key: string) => {
            onDelete(key);
          },
        }
      : null,
    [CUT]: moveNode
      ? {
          func: (key: string) => onCopyOrCut(key, true),
          condition: (node) => (node.key !== cuttingNodeKey ? true : cancelCutCopyAction(true)), // 本身就是被剪切的节点要被替换成取消
        }
      : null,
    [PASTE]:
      moveNode || copyNode
        ? {
            func: (key: string) => onPaste(key),
            condition: (node) =>
              (!!cuttingNodeKey || !!copyingNodeKey) &&
              cuttingNodeKey !== node.key &&
              copyingNodeKey !== node.key &&
              !isAncestor(treeData, node.key, cuttingNodeKey) &&
              !isAncestor(treeData, node.key, copyingNodeKey), // 如果当前节点是已被剪切或复制的节点的儿子,那么不能粘贴, 否则会循环引用
          }
        : null,
    [COPY]: copyNode
      ? {
          func: (key: string) => onCopyOrCut(key, false),
          condition: (node) => (node.key !== copyingNodeKey ? true : cancelCutCopyAction(false)), // 本身就是被复制的节点要被替换成取消
        }
      : null,
    [CUSTOM_EDIT]: {
      func: noop,
      hookFn: async (nodeKey: string, isCreate: boolean) => {
        // 用户自定义编辑,这可以提供用户自定义去创建文件和文件夹, 大多数情况是弹窗,所有func由用户自定义,然后透出hook,如果是创建行为则nodeKey本身是父,就用nodeKey用来刷新父目录,如果是Update行为,nodeKey是子,则要取出父来刷
        let parentKey = nodeKey;
        if (!isCreate) {
          const node = findTargetNode(nodeKey, treeData)!;
          parentKey = node.parentKey!;
        }
        await onLoadTreeData(parentKey);
        if (!expandedKeys.includes(parentKey)) {
          onExpand(expandedKeys.concat(parentKey));
        }
      },
    },
    [NEW_FILE]: createNode
      ? {
          func: (key: string) => createTreeNode(key, false),
          condition: (node) => node.key !== copyingNodeKey && node.key !== cuttingNodeKey, // 本身是被复制或剪切中的节点不能创建新节点
        }
      : null,
    [RENAME_FILE]: updateNode
      ? {
          func: (key: string) => renameTreeNode(key, false),
          condition: (node) => node.key !== copyingNodeKey && node.key !== cuttingNodeKey, // 本身是被复制或剪切中的节点不能重命名
        }
      : null,
  };

  const generateActions = (rawActions: TreeAction[], node: TreeNode): IAction[] => {
    const _actions = reduce(
      rawActions,
      (acc: IAction[], action) => {
        const { preset, func, node: actionNode, hasAuth = true, authTip } = action;
        if (preset) {
          const defaultFn =
            (
              fn: (key: string, n: TreeNodeNormal) => Promise<void>,
              hook?: (nodeKey: string, isCreate: boolean) => Promise<void>,
            ) =>
            async (key: string, n: TreeNodeNormal) => {
              await fn(key, n);
              func && func(key, n, hook);
              onPopMenuVisibleChange(key, false);
            };
          const presetAction = presetMap[preset]; // 策略模式取具体action
          if (presetAction) {
            const { node: presetNode, func: presetFunc, condition, hookFn } = presetAction;
            const addable = condition ? condition(node) : true; // condition为true则要添加
            if (!addable) {
              return acc;
            }
            if (typeof addable !== 'boolean') {
              return acc.concat(addable);
            }
            if (preset === DELETE && !hasAuth) {
              // 没权限的时候,去除删除的确认框
              return acc.concat({
                node: (
                  <div>
                    <WithAuth pass={hasAuth} noAuthTip={authTip}>
                      <span>{actionNode}</span>
                    </WithAuth>
                  </div>
                ),
                func: noop,
              });
            }
            return acc.concat({
              node: (
                <div>
                  <WithAuth pass={hasAuth} noAuthTip={authTip}>
                    <span>{presetNode || actionNode}</span>
                  </WithAuth>
                </div>
              ),
              func: hasAuth ? defaultFn(presetFunc, hookFn) : noop,
            });
          }
          return acc;
        }
        return func
          ? acc.concat({
              func: (curKey: string, curNode: TreeNodeNormal) => {
                func(curKey, curNode);
                onPopMenuVisibleChange(curKey, false);
              },
              node: actionNode,
            })
          : acc;
      },
      [],
    );
    return _actions;
  };

  const getActions = (node: TreeNodeNormal): IAction[] => {
    const execNode = node as TreeNode;
    if (execNode.disableAction) {
      return [];
    }
    if (node.isLeaf) {
      const fileActions =
        typeof actions?.fileActions === 'function' ? actions.fileActions(execNode) : actions?.fileActions;
      return generateActions(fileActions || [], execNode);
    }
    const folderActions =
      typeof actions?.folderActions === 'function' ? actions.folderActions(execNode) : actions?.folderActions;
    return generateActions(folderActions || [], execNode);
  };

  const onDrop = async (info: { dragNode: AntTreeNodeProps; node: AntTreeNodeProps }) => {
    const { dragNode, node: dropNode } = info;
    const dragKey = dragNode.key;
    let dropKey = dropNode.key;
    if (dropNode.isLeaf) {
      dropKey = dropNode.parentKey;
    }
    await handleMoveNode(dragKey, dropKey);
  };

  const operations = () => {
    if (titleOperation && Array.isArray(titleOperation)) {
      return map(titleOperation, (operation) => {
        if (operation.preset && operation.preset === NEW_FOLDER && createNode) {
          return newFolderOperation(async ({ name }: { name: string }) => {
            await createNode({ name, pinode: rootKey, type: 'd' });
            onLoadTreeData(rootKey);
          });
        }
        return operation;
      }) as Array<{ title: string }>;
    }
    return [];
  };

  const onSearch = async (query: string) => {
    if (!query || !fuzzySearch) {
      return;
    }
    const searchResult = await fuzzySearch({ fuzzy: query });
    const folderGroup = [] as Array<{ label: string; value: string }>;
    const fileGroup = [] as Array<{ label: string; value: string }>;
    forEach(searchResult, ({ isLeaf, key, titleAlias }) => {
      if (isLeaf) {
        fileGroup.push({ label: titleAlias, value: `leaf-${key}` });
      } else {
        folderGroup.push({ label: titleAlias, value: key });
      }
    });
    updater.filterOptions({ folderGroup, fileGroup });
  };

  const handleSearchChange = async (key?: string) => {
    if (!key) {
      updater.filterOptions({ folderGroup: [], fileGroup: [] });
      return;
    }
    const isLeaf = key.startsWith('leaf');
    const ancestors = await getAncestors({ inode: isLeaf ? key.slice(5) : key });
    const keys = map(ancestors, 'key');
    if (isLeaf) {
      updater.expandedKeys(Array.from(new Set(expandedKeys.concat(keys))));
      onSelectNode({ inode: key.slice(5), isLeaf });
    } else {
      updater.expandedKeys(Array.from(new Set(expandedKeys.concat(keys).concat(key))));
    }
  };

  const generateSearchOptions = () => {
    const { fileGroup, folderGroup } = filterOptions;
    const { file, folder } = searchGroup || { file: i18n.t('File'), folder: i18n.t('common:folder') };
    const options = [];
    if (folderGroup.length > 0) {
      options.push(
        <OptGroup key="folder" label={folder}>
          {map(folderGroup, ({ value, label }) => (
            <Option key={value} value={value}>
              {label}
            </Option>
          ))}
        </OptGroup>,
      );
    }
    if (fileGroup.length > 0) {
      options.push(
        <OptGroup key="file" label={file}>
          {map(fileGroup, ({ value, label }) => (
            <Option key={value} value={value}>
              {label}
            </Option>
          ))}
        </OptGroup>,
      );
    }
    return options;
  };

  return (
    <div>
      {title ? <Title title={title} showDivider={false} operations={operations()} /> : null}
      {fuzzySearch ? (
        <Select
          showSearch
          placeholder={i18n.t('common:Please enter the keyword to search')}
          showArrow={false}
          filterOption
          optionFilterProp={'children'}
          notFoundContent={null}
          onSearch={onSearch}
          onChange={handleSearchChange}
          className="w-full"
          allowClear
        >
          {generateSearchOptions()}
        </Select>
      ) : null}
      <Spin spinning={!!loading}>
        <Tree
          selectedKeys={currentKey ? [currentKey] : []}
          loadData={(node) => onLoadTreeData(node.key)}
          treeData={treeData}
          expandedKeys={expandedKeys}
          className="tree-category-container"
          blockNode
          showIcon
          onExpand={onExpand}
          onSelect={onClickNode}
          titleRender={(nodeData: TreeNodeNormal) => {
            const execNode = nodeData as TreeNode;
            return (
              <span className={`inline-block truncate ${execNode.disableAction ? 'w-full' : 'has-operates'}`}>
                {nodeData.title}
                {!execNode.disableAction && (
                  <Popover
                    content={getActions(nodeData).map((item, idx) => (
                      <div className="action-btn" key={`${idx}`} onClick={() => item.func?.(nodeData.key, nodeData)}>
                        {item.node}
                      </div>
                    ))}
                    footer={false}
                  >
                    <CustomIcon type="gd" className="tree-node-action" />
                  </Popover>
                )}
              </span>
            );
          }}
          draggable={!!moveNode && !cuttingNodeKey && !copyingNodeKey} // 当有剪切复制正在进行中时,不能拖动
          onDrop={onDrop}
          {...treeProps}
        />
      </Spin>
    </div>
  );
}
Example #28
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
export function TraceGraph(props: IProps) {
  const { dataSource } = props;
  const width = dataSource?.depth <= 12 ? 300 : dataSource?.depth * 24 + 100;
  const bg = ['#4E6097', '#498E9E', '#6CB38B', 'purple', '#F7A76B'];
  const errorColor = '#CE4324';
  // const bg = ['#5872C0', '#ADDD8B', '#DE6E6A', '#84BFDB', '#599F76', '#ED895D', '#9165AF','#DC84C8','#F3C96B'];
  const [expandedKeys, setExpandedKeys] = React.useState([] as string[]);
  const [selectedTimeRange, setSelectedTimeRange] = React.useState(null! as ITimeRange);
  const [proportion, setProportion] = React.useState([24, 0]);
  const [loading, setLoading] = React.useState(false);
  const [spanDetailData, setSpanDetailData] = React.useState({});
  const { roots, min, max } = listToTree(dataSource?.spans);
  const [tags, setTags] = React.useState(null! as MONITOR_TRACE.ITag);
  const [spanStartTime, setSpanStartTime] = React.useState(null! as number);
  const [timeRange, setTimeRange] = React.useState([null!, null!] as number[]);
  const [selectedSpanId, setSelectedSpanId] = React.useState(null! as string);
  const [view, setView] = React.useState('waterfall');
  const [spanData, spanDataLoading] = getSpanEvents.useState();
  const spanDataSource = spanData?.spanEvents || [];
  const duration = max - min;
  const allKeys: string[] = [];
  const { serviceAnalysis } = (spanDetailData as MONITOR_TRACE.ISpanRelationChart) || {};
  const [flameRef, { width: flameWidth }] = useMeasure();

  const containerRef = React.useRef(null);
  const [tooltipState, setTooltipState] = React.useState(
    null as { content: MONITOR_TRACE.FlameChartData; mouseX: number; mouseY: number } | null,
  );

  function getMousePos(
    relativeContainer: { getBoundingClientRect: () => DOMRect } | null,
    mouseEvent: { clientX: number; clientY: number },
  ) {
    if (relativeContainer !== null) {
      const rect = relativeContainer.getBoundingClientRect();
      const mouseX = mouseEvent.clientX - rect.left;
      const mouseY = mouseEvent.clientY - rect.top;
      return { mouseX, mouseY };
    } else {
      return { mouseX: 0, mouseY: 0 };
    }
  }

  const onMouseOver = (event: { clientX: number; clientY: number }, data: MONITOR_TRACE.FlameChartData) => {
    const { name, value, children, serviceName, selfDuration, spanKind, component } = data;
    setTooltipState({
      content: { name, value, children, serviceName, selfDuration, spanKind, component },
      ...getMousePos(containerRef.current, event),
    });
  };

  const onMouseOut = () => {
    setTooltipState(null);
  };

  const tooltipRef = useSmartTooltip({
    mouseX: tooltipState === null ? 0 : tooltipState.mouseX,
    mouseY: tooltipState === null ? 0 : tooltipState.mouseY,
  });

  const columns = [
    {
      title: i18n.t('Time'),
      dataIndex: 'timestamp',
      render: (time: number) => moment(time / 1000 / 1000).format('YYYY-MM-DD HH:mm:ss'),
    },
    {
      title: i18n.t('msp:Event'),
      dataIndex: 'events',
      ellipsis: true,
      render: (events: object) => (
        <div>
          {Object.keys(events).map((k) => (
            <Ellipsis title={`${k}: ${events[k]}`} key={k} />
          ))}
        </div>
      ),
    },
  ];

  const getMetaData = React.useCallback(async () => {
    setLoading(true);
    try {
      const { span_layer, span_kind, terminus_key, service_instance_id } = tags;
      const type = `${span_layer}_${span_kind}`;
      if (!service_instance_id) {
        return null;
      }
      const { success, data } = await getSpanAnalysis({
        type,
        startTime: timeRange[0],
        endTime: timeRange[1],
        serviceInstanceId: service_instance_id,
        tenantId: terminus_key,
      });

      if (success) {
        setSpanDetailData(data);
      }
    } finally {
      setLoading(false);
    }
  }, [tags, timeRange]);

  React.useEffect(() => {
    if (tags) {
      getMetaData();
    }
  }, [getMetaData, tags]);

  React.useEffect(() => {
    handleTableChange();
  }, [selectedSpanId, spanStartTime]);

  const handleTableChange = () => {
    if (selectedSpanId && spanStartTime) {
      getSpanEvents.fetch({
        startTime: Math.floor(spanStartTime),
        spanId: selectedSpanId,
      });
    }
  };

  const traverseData = (data: MONITOR_TRACE.ITraceSpan[]) => {
    for (let i = 0; i < data.length; i++) {
      data[i] = format(data[i], 0, handleClickTimeSpan);
    }

    return data;
  };
  const handleChangeView = (e: RadioChangeEvent) => {
    setView(e.target.value);
  };

  const treeData = traverseData(roots);
  const formatDashboardVariable = (conditions: string[]) => {
    const dashboardVariable = {};
    for (let i = 0; i < conditions?.length; i++) {
      dashboardVariable[conditions[i]] = tags?.[conditions[i]];
    }
    return dashboardVariable;
  };

  function handleClickTimeSpan(startTime: number, selectedTag: MONITOR_TRACE.ITag, id: string) {
    const r1 = moment(startTime / 1000 / 1000)
      .subtract(15, 'minute')
      .valueOf();
    const r2 = Math.min(
      moment(startTime / 1000 / 1000)
        .add(15, 'minute')
        .valueOf(),
      moment().valueOf(),
    );
    setSelectedTimeRange({
      mode: 'customize',
      customize: {
        start: moment(r1),
        end: moment(r2),
      },
    });
    setTimeRange([r1, r2]);
    setTags(selectedTag);
    setSpanStartTime(startTime / 1000 / 1000);
    setProportion([14, 10]);
    setSelectedSpanId(id);
  }

  function format(
    item: MONITOR_TRACE.ISpanItem,
    depth = 0,
    _handleClickTimeSpan: (startTime: number, selectedTag: MONITOR_TRACE.ITag, id: string) => void,
  ) {
    item.depth = depth;
    item.key = item.id;
    allKeys.push(item.id);
    const { startTime, endTime, duration: totalDuration, selfDuration, operationName, tags: _tags, id } = item;
    const { span_kind: spanKind, component, error, service_name: serviceName } = _tags;
    const leftRatio = (startTime - min) / duration;
    const centerRatio = (endTime - startTime) / duration;
    const rightRatio = (max - endTime) / duration;
    const showTextOnLeft = leftRatio > 0.2;
    const showTextOnRight = !showTextOnLeft && rightRatio > 0.2;
    const displayTotalDuration = mkDurationStr(totalDuration / 1000);
    item.title = (
      <div
        className="wrapper flex items-center"
        onClick={() => {
          _handleClickTimeSpan(startTime, _tags, id);
        }}
      >
        <Tooltip
          title={
            <SpanTitleInfo
              operationName={operationName}
              spanKind={spanKind}
              component={component}
              serviceName={serviceName}
            />
          }
        >
          <div className="left flex items-center" style={{ width: width - 24 * depth }}>
            <div className="w-1 h-4 relative mr-1" style={{ background: error ? errorColor : bg[depth % 5] }} />
            <div className="flex items-center w-full">
              <span className="font-semibold text-ms mr-2 whitespace-nowrap">{serviceName}</span>
              <span className="truncate text-xs">{operationName}</span>
            </div>
          </div>
        </Tooltip>
        <div className="right text-gray">
          <div style={{ flex: leftRatio }} className="text-right text-xs self-center">
            {showTextOnLeft && displayTotalDuration}
          </div>
          <Tooltip title={<SpanTimeInfo totalSpanTime={totalDuration} selfSpanTime={selfDuration} />}>
            <div
              style={{ flex: centerRatio < 0.01 ? 0.01 : centerRatio, background: error ? errorColor : bg[depth % 5] }}
              className="rounded-sm mx-1"
            />
          </Tooltip>
          <div style={{ flex: rightRatio }} className="self-center text-left text-xs">
            {showTextOnRight && displayTotalDuration}
          </div>
        </div>
      </div>
    );
    if (item.children) {
      item.children = item.children.map((x) => format(x, depth + 1, _handleClickTimeSpan));
    }
    return item;
  }

  const formatFlameData = () => {
    let flameData = {} as MONITOR_TRACE.FlameChartData;
    if (roots?.length === 1) {
      const { operationName, duration: totalDuration, tags: _tags, selfDuration } = roots[0];
      const { service_name, span_kind, component } = _tags;
      flameData = {
        name: operationName,
        value: totalDuration,
        children: [],
        serviceName: service_name,
        selfDuration,
        spanKind: span_kind,
        component,
      };
      forEach(roots[0].children, (span) => flameData.children.push(formatFlameDataChild(span)));
    } else {
      flameData = {
        name: 'root',
        value: dataSource?.duration,
        children: [],
        serviceName: '',
        selfDuration: dataSource?.duration,
        spanKind: '',
        component: '',
      };
      forEach(roots, (span) => flameData.children.push(formatFlameDataChild(span)));
    }
    return flameData;
  };

  const formatFlameDataChild = (span: MONITOR_TRACE.ISpanItem) => {
    let node = {} as MONITOR_TRACE.FlameChartData;
    const { operationName, duration: totalDuration, tags: _tags, selfDuration } = span;
    const { service_name, span_kind, component } = _tags;
    node = {
      name: operationName,
      value: totalDuration,
      children: [],
      serviceName: service_name,
      selfDuration,
      spanKind: span_kind,
      component,
    };
    if (span && span.children) {
      for (const item of span.children) {
        const child = formatFlameDataChild(item);
        node.children.push(child);
      }
    }
    return node;
  };

  const onExpand = (keys: string[]) => {
    setExpandedKeys(keys);
  };

  return (
    <>
      <TraceDetailInfo dataSource={dataSource} />
      <RadioGroup defaultValue="waterfall" value={view} onChange={handleChangeView} className="flex justify-end">
        <RadioButton value="waterfall">
          <span className="flex items-center">
            <ErdaIcon className="mr-1" type="pubutu" color="currentColor" />
            {i18n.t('msp:Waterfall Chart')}
          </span>
        </RadioButton>
        <RadioButton value="flame">
          <span className="flex items-center">
            <ErdaIcon className="mr-1" type="huoyantu" color="currentColor" />
            {i18n.t('msp:Flame Graph')}
          </span>
        </RadioButton>
      </RadioGroup>
      <div className="mt-4 trace-span-detail" ref={flameRef}>
        {view === 'waterfall' && (
          <Row gutter={20}>
            <Col span={proportion[0]} className={`${proportion[0] !== 24 ? 'pr-0' : ''}`}>
              <TraceHeader
                duration={duration}
                width={width}
                setExpandedKeys={setExpandedKeys}
                allKeys={allKeys}
                expandedKeys={expandedKeys}
              />
              <div className="trace-graph">
                {treeData.length > 0 && (
                  <Tree
                    showLine={{ showLeafIcon: false }}
                    defaultExpandAll
                    height={window.innerHeight - 200}
                    // switcherIcon={<DownOutlined />}
                    // switcherIcon={<CustomIcon type="caret-down" />}
                    expandedKeys={expandedKeys}
                    treeData={treeData}
                    onExpand={onExpand}
                  />
                )}
              </div>
            </Col>
            <Col span={proportion[1]} className={`${proportion[0] !== 24 ? 'pl-0' : ''}`}>
              <div className="flex justify-between items-center my-2 px-3 py-1">
                <div className="text-sub text-sm font-semibold w-5/6">
                  <Ellipsis title={tags?.operation_name}>{tags?.operation_name}</Ellipsis>
                </div>
                <Tooltip title={i18n.t('close')}>
                  <span onClick={() => setProportion([24, 0])} className="cursor-pointer">
                    <CustomIcon type="gb" className="text-holder" />
                  </span>
                </Tooltip>
              </div>
              <div className="px-3">
                {selectedTimeRange && (
                  <TimeSelect
                    // defaultValue={globalTimeSelectSpan.data}
                    // className={className}
                    onChange={(data, range) => {
                      if (Object.keys(data)?.length !== 0) {
                        setSelectedTimeRange(data);
                      }
                      const { quick = '' } = data;
                      let range1 = range?.[0]?.valueOf() || selectedTimeRange?.customize?.start?.valueOf();
                      let range2 = range?.[1]?.valueOf() || selectedTimeRange?.customize?.end?.valueOf();
                      if (quick) {
                        const [unit, count] = quick.split(':');
                        const [start, end] = translateRelativeTime(unit, Number(count));
                        range1 = start?.valueOf();
                        range2 = Math.min(end?.valueOf(), moment().valueOf());
                      }
                      setTimeRange([range1, range2]);
                    }}
                    value={selectedTimeRange}
                  />
                )}
              </div>
              {(serviceAnalysis || proportion[0] === 14) && (
                <div className="px-3 trace-detail-chart" style={{ height: window.innerHeight - 200 }}>
                  <Tabs>
                    <TabPane tab={i18n.t('msp:Attributes')} key={1}>
                      <KeyValueList data={tags} />
                    </TabPane>
                    <TabPane tab={i18n.t('msp:Events')} key={2}>
                      <Spin spinning={spanDataLoading}>
                        <ErdaTable columns={columns} dataSource={spanDataSource} onChange={handleTableChange} />
                      </Spin>
                    </TabPane>
                    <TabPane tab={i18n.t('msp:Related Services')} key={3}>
                      {!serviceAnalysis?.dashboardId && <EmptyHolder relative />}
                      {serviceAnalysis?.dashboardId && (
                        <ServiceListDashboard
                          timeSpan={{ startTimeMs: timeRange[0], endTimeMs: timeRange[1] }}
                          dashboardId={serviceAnalysis?.dashboardId}
                          extraGlobalVariable={formatDashboardVariable(serviceAnalysis?.conditions)}
                        />
                      )}
                    </TabPane>
                  </Tabs>
                </div>
              )}
            </Col>
          </Row>
        )}

        {view === 'flame' && (
          <div ref={containerRef} className="relative graph-flame overflow-y-auto overflow-x-hidden">
            <FlameGraph
              data={formatFlameData()}
              height={dataSource ? 20 * dataSource.depth + 1 : 200}
              width={flameWidth}
              onMouseOver={onMouseOver}
              onMouseOut={onMouseOut}
              disableDefaultTooltips
            />
            {tooltipState !== null && (
              <div ref={tooltipRef} className="absolute bg-default p-2 shadow-lg break-words rounded-[3px]">
                <SpanTitleInfo
                  operationName={tooltipState?.content.name}
                  spanKind={tooltipState?.content.spanKind}
                  component={tooltipState?.content.component}
                  serviceName={tooltipState?.content.serviceName}
                />
                <div className="text-white">
                  {i18n.t('current')} span {mkDurationStr(tooltipState?.content.selfDuration / 1000)} -{' '}
                  {i18n.t('total')} span {mkDurationStr(tooltipState?.content.value / 1000)}
                </div>
              </div>
            )}
          </div>
        )}
      </div>
    </>
  );
}
Example #29
Source File: version-info.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
VersionInfo = ({ assetID, onRelation, onSelectVersion, versionRef }: IProps) => {
  const params = routeInfoStore.useStore((s) => s.params);
  const { getVersionTree, getListOfVersions, getInstance, deleteAssetVersion, updateAssetVersion } =
    apiMarketStore.effects;
  const [assetVersionList, versionTree, instance, assetDetail, instancePermission] = apiMarketStore.useStore((s) => [
    s.assetVersionList,
    s.versionTree,
    s.instance,
    s.assetDetail,
    s.instancePermission,
  ]);
  const creatorID = get(assetDetail, ['asset', 'creatorID']);
  const instanceUrl = get(instance, 'url');
  const [state, updater, update] = useUpdate<IState>({
    chooseVersionInfo: {},
    expandedKeys: [],
    versionItem: {},
    showExport: false,
  });
  const refreshVersionTree = React.useCallback(() => {
    if (!assetID) {
      return;
    }
    getVersionTree({ assetID, instantiation: false, patch: false, access: false }).then(({ list }) => {
      const swaggerVersion = get(list, ['0', 'swaggerVersion']);
      const major = get(list, ['0', 'versions', '0', 'major']);
      const minor = get(list, ['0', 'versions', '0', 'minor']);
      getListOfVersions({ assetID, major, minor, spec: false });
      getInstance({ swaggerVersion, assetID, minor, major });
      const temp = {
        major,
        minor,
        swaggerVersion,
        selectedKeys: [`${swaggerVersion}-${major}.${minor}`],
      };
      update({
        chooseVersionInfo: temp,
        expandedKeys: [swaggerVersion],
      });
      onSelectVersion && onSelectVersion(temp);
    });
  }, [assetID, getInstance, getListOfVersions, getVersionTree, onSelectVersion, update]);
  React.useEffect(() => {
    refreshVersionTree();
  }, [refreshVersionTree]);
  useImperativeHandle(versionRef, () => ({
    handleTreeSelect,
  }));
  const handleTreeSelect = ({
    swaggerVersion,
    major,
    minor,
  }: {
    swaggerVersion: string;
    major: number;
    minor: number;
  }) => {
    const temp = {
      major,
      minor,
      swaggerVersion,
      selectedKeys: [`${swaggerVersion}-${major}.${minor}`],
    };
    update({
      chooseVersionInfo: temp,
      expandedKeys: [...state.expandedKeys, swaggerVersion],
    });
  };
  const goToDetail = ({ id }: API_MARKET.AssetVersion) => {
    goTo(goTo.pages.apiManageAssetDetail, { assetID, versionID: id, scope: params.scope });
  };
  const closeModal = () => {
    updater.showExport(false);
  };
  const handleExport = (record: API_MARKET.AssetVersion) => {
    update({
      showExport: true,
      versionItem: record,
    });
  };
  const handleExpand = (expandedKeys: string[]) => {
    updater.expandedKeys(expandedKeys);
  };
  const handleSelectVersion = (selectedKeys: string[], { selected, node }: AntTreeNodeSelectedEvent) => {
    if (!selected) {
      return;
    }
    const { major, minor, swaggerVersion } = node.props;
    const temp = {
      major,
      minor,
      selectedKeys,
      swaggerVersion,
    };
    if (state.chooseVersionInfo.swaggerVersion !== swaggerVersion || state.chooseVersionInfo.minor !== minor) {
      getInstance({ assetID, major, minor, swaggerVersion });
    }
    updater.chooseVersionInfo(temp);
    onSelectVersion && onSelectVersion(temp);
    getListOfVersions({ assetID, major, minor, spec: false });
  };
  const handleDeleteVersion = ({ id }: API_MARKET.AssetVersion, e: React.MouseEvent<HTMLSpanElement>) => {
    e.stopPropagation();
    const { major, minor } = state.chooseVersionInfo;
    Modal.confirm({
      title: i18n.t('default:confirm to delete the current version?'),
      onOk: async () => {
        await deleteAssetVersion({ versionID: id, assetID });
        // 当前minor中还有patch版本
        if (assetVersionList.length > 1) {
          getListOfVersions({ assetID, major, minor, spec: false });
        } else {
          // 当前minor中patch全部删除
          // 刷新左侧版本树
          refreshVersionTree();
        }
      },
    });
  };
  const toggleDeprecated = (
    { id, deprecated, major, minor, patch, assetName }: API_MARKET.AssetVersion,
    e: React.MouseEvent<HTMLSpanElement>,
  ) => {
    e.stopPropagation();
    const name = `${assetName} ${major}.${minor}.${patch}`;
    let icon: string | undefined = 'warning';
    let title = i18n.t('deprecated version');
    let content = i18n.t('Are you sure you want to deprecate {name}?', { name });
    if (deprecated) {
      icon = undefined;
      title = i18n.t('revert deprecated version');
      content = i18n.t('Are you sure you want to revert the deprecated status of {name}?', { name });
    }
    Modal.confirm({
      title,
      content,
      icon,
      onOk: () => {
        updateAssetVersion({ assetID, versionID: id, deprecated: !deprecated }).then(() => {
          getListOfVersions({ assetID, major, minor, spec: false });
        });
      },
    });
  };
  const columns: Array<ColumnProps<API_MARKET.VersionItem>> = [
    {
      title: i18n.t('default:version number'),
      dataIndex: ['version', 'major'],
      width: 120,
      render: (_text, { version: { major, minor, patch } }) => `${major}.${minor}.${patch}`,
    },
    {
      title: i18n.t('API description document protocol'),
      dataIndex: ['version', 'specProtocol'],
      render: (text) => protocolMap[text].fullName,
    },
    {
      title: i18n.t('Creator'),
      dataIndex: ['version', 'creatorID'],
      width: 120,
      render: (text) => <Avatar showName name={<UserInfo id={text} />} />,
    },
    {
      title: i18n.t('Creation time'),
      dataIndex: ['version', 'createdAt'],
      width: 200,
      render: (text) => (text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : ''),
    },
    {
      title: i18n.t('Operations'),
      dataIndex: ['version', 'id'],
      width: 280,
      fixed: 'right',
      render: (_text, { version }) => (
        <TableActions>
          <span
            onClick={(e) => {
              handleExport(version, e);
            }}
          >
            {i18n.t('Export')}
          </span>
          <UnityAuthWrap wrap={false} userID={creatorID} path={['apiMarket', 'deleteVersion']}>
            <span
              onClick={(e) => {
                handleDeleteVersion(version, e);
              }}
            >
              {i18n.t('Delete')}
            </span>
          </UnityAuthWrap>
          <UnityAuthWrap wrap={false} userID={creatorID} path={['apiMarket', 'addVersion']}>
            <span
              onClick={(e) => {
                toggleDeprecated(version, e);
              }}
            >
              {version.deprecated ? i18n.t('revert deprecated version') : i18n.t('deprecated version')}
            </span>
          </UnityAuthWrap>
        </TableActions>
      ),
    },
  ];
  const treeData = React.useMemo(() => formatVersionTree(versionTree), [versionTree]);
  const handleRelation = () => {
    if (instancePermission.edit === false) {
      Modal.info({
        title: i18n.t(
          'The current version has been referenced by the management entry. Please dereference before editing.',
        ),
      });
      return;
    }
    onRelation('instance');
  };
  return (
    <div className="flex justify-between items-start content-wrap relative">
      <div className="left pr-4">
        <Tree
          blockNode
          defaultExpandParent
          selectedKeys={state.chooseVersionInfo.selectedKeys}
          expandedKeys={state.expandedKeys}
          treeData={treeData}
          onSelect={handleSelectVersion}
          onExpand={handleExpand}
        />
      </div>
      <div className="right flex-1 pl-4">
        <div className="flex justify-between items-center">
          <div className="title text-normal font-medium text-base my-3">{i18n.t('related instance')}</div>
          <UnityAuthWrap userID={creatorID} path={['apiMarket', 'relatedInstance']}>
            <Button onClick={handleRelation}>{i18n.t('Edit')}</Button>
          </UnityAuthWrap>
        </div>
        {instance.type === 'dice' ? (
          <>
            <div className="text-desc instance-label">{i18n.t('Service name')}</div>
            <div className="text-sub font-medium instance-name mb-3">{get(instance, 'serviceName', '-')}</div>
            <div className="text-desc instance-label">{i18n.t('msp:deployment branch')}</div>
            <div className="text-sub font-medium instance-name mb-3">{get(instance, 'runtimeName', '-')}</div>
          </>
        ) : null}
        <div className="text-desc instance-label">{i18n.t('related instance')}</div>
        <div className="text-sub font-medium instance-name mb-6">{instanceUrl || '-'}</div>
        <div className="title text-normal font-medium text-base mb-3">{i18n.t('version list')}</div>
        <Table<API_MARKET.VersionItem>
          rowKey={({ version: { major, minor, patch } }) => `${major}-${minor}-${patch}`}
          columns={columns}
          dataSource={assetVersionList}
          pagination={false}
          onRow={({ version }) => {
            return {
              onClick: () => {
                goToDetail(version);
              },
            };
          }}
          scroll={{ x: 800 }}
        />
      </div>
      <ExportFile
        visible={state.showExport}
        versionID={state.versionItem.id}
        assetID={state.versionItem.assetID}
        specProtocol={state.versionItem.specProtocol}
        onCancel={closeModal}
      />
    </div>
  );
}