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 |
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 |
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 |
{ DirectoryTree, TreeNode } = Tree
Example #4
Source File: RoutesView.spec.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
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 |
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 |
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 |
{ DirectoryTree } = Tree
Example #8
Source File: TreeFilter.tsx From datart with Apache License 2.0 | 5 votes |
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 |
{ TreeNode } = Tree
Example #10
Source File: api-doc-tree.tsx From erda-ui with GNU Affero General Public License v3.0 | 5 votes |
{ TreeNode } = Tree
Example #11
Source File: index.tsx From gant-design with MIT License | 5 votes |
{ TreeNode } = Tree
Example #12
Source File: index.tsx From erda-ui with GNU Affero General Public License v3.0 | 5 votes |
{ DirectoryTree, TreeNode } = Tree
Example #13
Source File: SystemInfoTree.tsx From disco-cube-admin with MIT License | 5 votes |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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);
});
});
// 记录当前所有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];
});
// @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节点和选中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);
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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>
);
}