node 自适应宽度
This commit is contained in:
@@ -451,8 +451,7 @@ function create_flow_nodes(data: Topology_Data, view: Topology_View, layout: Rec
|
|||||||
positions.set(node.id, {x: source.x + index * 270, y: source.y + node_size(source_node)[1] + 92})
|
positions.set(node.id, {x: source.x + index * 270, y: source.y + node_size(source_node)[1] + 92})
|
||||||
}
|
}
|
||||||
return data.nodes.map(node => {
|
return data.nodes.map(node => {
|
||||||
const [width] = node_size(node)
|
return {id: node.id, type: "topology", position: positions.get(node.id)!, draggable: true, selectable: true, sourcePosition: Position.Bottom, targetPosition: Position.Top, data: {topology_node: node, kind: kinds.get(node.kind), status, status_expansion}}
|
||||||
return {id: node.id, type: "topology", position: positions.get(node.id)!, draggable: true, selectable: true, sourcePosition: Position.Bottom, targetPosition: Position.Top, style: {width}, data: {topology_node: node, kind: kinds.get(node.kind), status, status_expansion}}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function create_flow_edges(data: Topology_Data, schema: Data_Topology_Schema, status_expansion: MutableRefObject<Set<string>>, on_context_menu: (event: React_Mouse_Event, edge_id: string) => void): Flow_Edge[] {
|
function create_flow_edges(data: Topology_Data, schema: Data_Topology_Schema, status_expansion: MutableRefObject<Set<string>>, on_context_menu: (event: React_Mouse_Event, edge_id: string) => void): Flow_Edge[] {
|
||||||
@@ -513,22 +512,47 @@ function status_children(value: unknown): [string, unknown][] {
|
|||||||
return value.map((item, index) => [String(index), item])
|
return value.map((item, index) => [String(index), item])
|
||||||
return value !== null && typeof value === "object" ? Object.entries(value as Record<string, unknown>) : []
|
return value !== null && typeof value === "object" ? Object.entries(value as Record<string, unknown>) : []
|
||||||
}
|
}
|
||||||
|
function status_text_width(value: string): number {
|
||||||
|
return Array.from(value).reduce((width, character) => width + (character.charCodeAt(0) > 255 ? 11 : 6.4), 0)
|
||||||
|
}
|
||||||
|
function status_tree_width(value: Record<string, unknown>, minimum: number): number {
|
||||||
|
let required = minimum
|
||||||
|
const visit = (name: string, child: unknown, depth: number) => {
|
||||||
|
const children = status_children(child)
|
||||||
|
const display = children.length > 0 ? `${Array.isArray(child) ? "[" : "{"}${children.length}${Array.isArray(child) ? "]" : "}"}` : status_text(child)
|
||||||
|
required = Math.max(required, depth * 20 + 20 + status_text_width(name) + 18 + status_text_width(display) + 16)
|
||||||
|
for(const [key, nested] of children)
|
||||||
|
visit(key, nested, depth + 1)
|
||||||
|
}
|
||||||
|
for(const [name, child] of Object.entries(value))
|
||||||
|
visit(name, child, 0)
|
||||||
|
return Math.min(920, Math.ceil(required))
|
||||||
|
}
|
||||||
|
function use_maximum_status_width(value: Record<string, unknown>, minimum: number): number {
|
||||||
|
const required = useMemo(() => status_tree_width(value, minimum), [minimum, value])
|
||||||
|
const [maximum, set_maximum] = useState(required)
|
||||||
|
useEffect(() => {
|
||||||
|
if(required > maximum)
|
||||||
|
set_maximum(required)
|
||||||
|
}, [maximum, required])
|
||||||
|
return Math.max(maximum, required)
|
||||||
|
}
|
||||||
function Status_Tree_Node({name, value, depth}: {name: string, value: unknown, depth: number}) {
|
function Status_Tree_Node({name, value, depth}: {name: string, value: unknown, depth: number}) {
|
||||||
const children = status_children(value)
|
const children = status_children(value)
|
||||||
const branch = children.length > 0
|
const branch = children.length > 0
|
||||||
const [expanded, set_expanded] = useState(depth === 0)
|
const [expanded, set_expanded] = useState(depth === 0)
|
||||||
return <div style={{fontSize: 11}}>
|
return <div style={{fontSize: 11, width: "max-content"}}>
|
||||||
<div title={branch ? undefined : status_text(value)} style={{display: "flex", alignItems: "flex-start", minHeight: 22, paddingLeft: depth * 11}}>
|
<div title={branch ? undefined : status_text(value)} style={{display: "grid", gridTemplateColumns: "20px max-content max-content", columnGap: 9, alignItems: "start", minHeight: 22, paddingLeft: depth * 11, width: "max-content"}}>
|
||||||
{branch ? <Button type="text" size="small" icon={expanded ? <CaretDownOutlined/> : <CaretRightOutlined/>} title={expanded ? "收起分支" : "展开分支"} onClick={() => set_expanded(!expanded)} style={{flex: "none", width: 20, minWidth: 20, height: 20, padding: 0}}/> : <span style={{width: 20, flex: "none", color: "#d9d9d9", lineHeight: "20px", textAlign: "center"}}>•</span>}
|
{branch ? <Button type="text" size="small" icon={expanded ? <CaretDownOutlined/> : <CaretRightOutlined/>} title={expanded ? "收起分支" : "展开分支"} onClick={() => set_expanded(!expanded)} style={{flex: "none", width: 20, minWidth: 20, height: 20, padding: 0}}/> : <span style={{width: 20, flex: "none", color: "#d9d9d9", lineHeight: "20px", textAlign: "center"}}>•</span>}
|
||||||
<span style={{color: "#667085", flex: "0 0 42%", minWidth: 0, lineHeight: "20px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"}}>{name}</span>
|
<span style={{color: "#667085", lineHeight: "20px", whiteSpace: "nowrap"}}>{name}</span>
|
||||||
<span style={{flex: 1, minWidth: 0, lineHeight: "20px", color: branch ? "#98a2b3" : "#101828", overflowWrap: "anywhere"}}>{branch ? `${Array.isArray(value) ? "[" : "{"}${children.length}${Array.isArray(value) ? "]" : "}"}` : status_text(value)}</span>
|
<span style={{lineHeight: "20px", color: branch ? "#98a2b3" : "#101828", whiteSpace: "nowrap"}}>{branch ? `${Array.isArray(value) ? "[" : "{"}${children.length}${Array.isArray(value) ? "]" : "}"}` : status_text(value)}</span>
|
||||||
</div>
|
</div>
|
||||||
{branch && expanded && <div style={{marginLeft: depth * 11 + 9, borderLeft: "1px solid #d0d5dd"}}>{children.map(([key, child]) => <Status_Tree_Node key={key} name={key} value={child} depth={depth + 1}/>)}</div>}
|
{branch && expanded && <div style={{marginLeft: depth * 11 + 9, borderLeft: "1px solid #d0d5dd"}}>{children.map(([key, child]) => <Status_Tree_Node key={key} name={key} value={child} depth={depth + 1}/>)}</div>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
function Status_Tree({value}: {value: Record<string, unknown>}) {
|
function Status_Tree({value}: {value: Record<string, unknown>}) {
|
||||||
const entries = Object.entries(value)
|
const entries = Object.entries(value)
|
||||||
return entries.length > 0 ? <div>{entries.map(([key, child]) => <Status_Tree_Node key={key} name={key} value={child} depth={0}/>)}</div> : <Typography.Text type="secondary" style={{fontSize: 11}}>暂无状态</Typography.Text>
|
return entries.length > 0 ? <div style={{width: "max-content", minWidth: "100%"}}>{entries.map(([key, child]) => <Status_Tree_Node key={key} name={key} value={child} depth={0}/>)}</div> : <Typography.Text type="secondary" style={{fontSize: 11}}>暂无状态</Typography.Text>
|
||||||
}
|
}
|
||||||
function use_polling_status(api: string | undefined, expanded: boolean, poll_interval_ms: number) {
|
function use_polling_status(api: string | undefined, expanded: boolean, poll_interval_ms: number) {
|
||||||
const [status, set_status] = useState<Record<string, unknown> | null>(null)
|
const [status, set_status] = useState<Record<string, unknown> | null>(null)
|
||||||
@@ -583,9 +607,10 @@ function Node_Status({node, config, expansion}: {node: Topology_Node, config: To
|
|||||||
const [expanded, set_expanded] = use_status_expansion(`node:${node.id}`, expansion)
|
const [expanded, set_expanded] = use_status_expansion(`node:${node.id}`, expansion)
|
||||||
const {status, loading, error, updated_at} = use_polling_status(node.status_api, expanded, config.poll_interval_ms)
|
const {status, loading, error, updated_at} = use_polling_status(node.status_api, expanded, config.poll_interval_ms)
|
||||||
const current_status = node.status_api ? status ?? {} : Object.fromEntries(node.details.map(detail => [detail.label, detail.value]))
|
const current_status = node.status_api ? status ?? {} : Object.fromEntries(node.details.map(detail => [detail.label, detail.value]))
|
||||||
|
const status_width = use_maximum_status_width(current_status, node_size(node)[0] - 28)
|
||||||
return <div className="nodrag nopan" data-status-loading={expanded && loading} onPointerDown={event => event.stopPropagation()} onClick={event => event.stopPropagation()} style={{marginTop: 7, borderTop: "1px solid rgba(0,0,0,.08)", paddingTop: 5}}>
|
return <div className="nodrag nopan" data-status-loading={expanded && loading} onPointerDown={event => event.stopPropagation()} onClick={event => event.stopPropagation()} style={{marginTop: 7, borderTop: "1px solid rgba(0,0,0,.08)", paddingTop: 5}}>
|
||||||
<div style={{display: "flex", alignItems: "center", justifyContent: "space-between"}}><Button type="text" size="small" icon={expanded ? <CaretDownOutlined/> : <CaretRightOutlined/>} title={expanded ? "收起后停止轮询" : config.collapsed_hint} onClick={() => set_expanded(!expanded)} style={{height: 24, padding: "0 4px"}}>{expanded ? "收起状态" : "展开状态"}</Button>{expanded && <Button type="text" size="small" icon={<CopyOutlined/>} title="复制当前状态" disabled={loading || Boolean(error)} onClick={() => void copy_status(current_status)} style={{height: 24, padding: "0 4px"}}>复制</Button>}</div>
|
<div style={{display: "flex", alignItems: "center", justifyContent: "space-between"}}><Button type="text" size="small" icon={expanded ? <CaretDownOutlined/> : <CaretRightOutlined/>} title={expanded ? "收起后停止轮询" : config.collapsed_hint} onClick={() => set_expanded(!expanded)} style={{height: 24, padding: "0 4px"}}>{expanded ? "收起状态" : "展开状态"}</Button>{expanded && <Button type="text" size="small" icon={<CopyOutlined/>} title="复制当前状态" disabled={loading || Boolean(error)} onClick={() => void copy_status(current_status)} style={{height: 24, padding: "0 4px"}}>复制</Button>}</div>
|
||||||
{expanded && <div style={{marginTop: 5, padding: 7, background: "rgba(255,255,255,.72)", borderRadius: 6, fontSize: 11}}>
|
{expanded && <div style={{width: status_width, maxWidth: "min(920px, calc(100vw - 96px))", boxSizing: "border-box", marginTop: 5, padding: 7, background: "rgba(255,255,255,.72)", borderRadius: 6, fontSize: 11}}>
|
||||||
{loading && <div style={{padding: 5, textAlign: "center"}}><Spin size="small"/></div>}
|
{loading && <div style={{padding: 5, textAlign: "center"}}><Spin size="small"/></div>}
|
||||||
{error && <Typography.Text type="danger" style={{fontSize: 11}}>{error}</Typography.Text>}
|
{error && <Typography.Text type="danger" style={{fontSize: 11}}>{error}</Typography.Text>}
|
||||||
{!loading && !error && <Status_Tree value={current_status}/>}
|
{!loading && !error && <Status_Tree value={current_status}/>}
|
||||||
@@ -599,11 +624,12 @@ function Topology_Flow_Edge({id, sourceX, sourceY, targetX, targetY, sourcePosit
|
|||||||
const [expanded, set_expanded] = use_status_expansion(`edge:${edge.id}`, data!.status_expansion)
|
const [expanded, set_expanded] = use_status_expansion(`edge:${edge.id}`, data!.status_expansion)
|
||||||
const {status, loading, error, updated_at} = use_polling_status(edge.status_api, expanded, data!.status.poll_interval_ms)
|
const {status, loading, error, updated_at} = use_polling_status(edge.status_api, expanded, data!.status.poll_interval_ms)
|
||||||
const current_status = edge.status_api ? status ?? {} : Object.fromEntries((edge.details ?? []).map(detail => [detail.label, detail.value]))
|
const current_status = edge.status_api ? status ?? {} : Object.fromEntries((edge.details ?? []).map(detail => [detail.label, detail.value]))
|
||||||
|
const status_width = use_maximum_status_width(current_status, 280)
|
||||||
return <>
|
return <>
|
||||||
<BaseEdge id={id} path={edge_path} markerEnd={markerEnd} style={style} interactionWidth={interactionWidth}/>
|
<BaseEdge id={id} path={edge_path} markerEnd={markerEnd} style={style} interactionWidth={interactionWidth}/>
|
||||||
<EdgeLabelRenderer><div className="ecap-topology-edge-status nodrag nopan" data-edge-id={edge.id} data-edge-status-expanded={expanded} data-status-loading={expanded && loading} onContextMenu={event => data!.on_context_menu(event)} style={{position: "absolute", transform: `translate(-50%, -50%) translate(${label_x}px, ${label_y}px)`, pointerEvents: "all", zIndex: expanded ? 8 : 2}}>
|
<EdgeLabelRenderer><div className="ecap-topology-edge-status nodrag nopan" data-edge-id={edge.id} data-edge-status-expanded={expanded} data-status-loading={expanded && loading} onContextMenu={event => data!.on_context_menu(event)} style={{position: "absolute", transform: `translate(-50%, -50%) translate(${label_x}px, ${label_y}px)`, pointerEvents: "all", zIndex: expanded ? 8 : 2}}>
|
||||||
{!expanded && <Button shape="circle" size="small" icon={<CaretRightOutlined/>} title="展开连接状态" onClick={() => set_expanded(true)} style={{width: 24, minWidth: 24, height: 24, color: edge.active ? "#52c41a" : "#8c8c8c", boxShadow: "0 2px 7px rgba(0,0,0,.16)"}}/>}
|
{!expanded && <Button shape="circle" size="small" icon={<CaretRightOutlined/>} title="展开连接状态" onClick={() => set_expanded(true)} style={{width: 24, minWidth: 24, height: 24, color: edge.active ? "#52c41a" : "#8c8c8c", boxShadow: "0 2px 7px rgba(0,0,0,.16)"}}/>}
|
||||||
{expanded && <div style={{width: 280, padding: 9, background: "rgba(255,255,255,.96)", border: `1px solid ${edge.active ? "#95de64" : "#d9d9d9"}`, borderRadius: 8, boxShadow: "0 5px 18px rgba(0,0,0,.18)", fontSize: 11}}>
|
{expanded && <div style={{width: status_width, maxWidth: "min(920px, calc(100vw - 96px))", boxSizing: "border-box", padding: 9, background: "rgba(255,255,255,.96)", border: `1px solid ${edge.active ? "#95de64" : "#d9d9d9"}`, borderRadius: 8, boxShadow: "0 5px 18px rgba(0,0,0,.18)", fontSize: 11}}>
|
||||||
<div style={{display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6}}><Typography.Text strong style={{fontSize: 12}}>{edge.kind === "runtime" ? "客户端连接状态" : "数据连接状态"}</Typography.Text><Space size={2}><Button type="text" size="small" icon={<CopyOutlined/>} title="复制当前状态" disabled={loading || Boolean(error)} onClick={() => void copy_status(current_status)} style={{height: 22, padding: "0 4px"}}/><Button type="text" size="small" icon={<CaretDownOutlined/>} title="收起并停止轮询" onClick={() => set_expanded(false)} style={{height: 22, padding: "0 4px"}}/></Space></div>
|
<div style={{display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6}}><Typography.Text strong style={{fontSize: 12}}>{edge.kind === "runtime" ? "客户端连接状态" : "数据连接状态"}</Typography.Text><Space size={2}><Button type="text" size="small" icon={<CopyOutlined/>} title="复制当前状态" disabled={loading || Boolean(error)} onClick={() => void copy_status(current_status)} style={{height: 22, padding: "0 4px"}}/><Button type="text" size="small" icon={<CaretDownOutlined/>} title="收起并停止轮询" onClick={() => set_expanded(false)} style={{height: 22, padding: "0 4px"}}/></Space></div>
|
||||||
{loading && <div style={{padding: 5, textAlign: "center"}}><Spin size="small"/></div>}
|
{loading && <div style={{padding: 5, textAlign: "center"}}><Spin size="small"/></div>}
|
||||||
{error && <Typography.Text type="danger" style={{fontSize: 11}}>{error}</Typography.Text>}
|
{error && <Typography.Text type="danger" style={{fontSize: 11}}>{error}</Typography.Text>}
|
||||||
@@ -618,7 +644,7 @@ function Topology_Flow_Node({data}: NodeProps<Flow_Node>) {
|
|||||||
const [width] = node_size(node)
|
const [width] = node_size(node)
|
||||||
return <>
|
return <>
|
||||||
{node.kind !== "data_source" && <Handle type="target" position={Position.Top} isConnectable={node.kind === "data_feed"} title={node.kind === "data_feed" ? "拖入数据源连接" : undefined} style={{zIndex: 5, width: 11, height: 11, background: data.kind?.stroke ?? "#8c8c8c", border: "2px solid #fff"}}/>}
|
{node.kind !== "data_source" && <Handle type="target" position={Position.Top} isConnectable={node.kind === "data_feed"} title={node.kind === "data_feed" ? "拖入数据源连接" : undefined} style={{zIndex: 5, width: 11, height: 11, background: data.kind?.stroke ?? "#8c8c8c", border: "2px solid #fff"}}/>}
|
||||||
<div className="ecap-topology-node" style={{position: "relative", width, boxSizing: "border-box", border: `2px solid ${data.kind?.stroke ?? "#8c8c8c"}`, borderRadius: 12, background: data.kind?.fill ?? "#fff", opacity: node.enabled ? 1 : 0.58, boxShadow: "0 5px 16px rgba(0,0,0,.10)", padding: "11px 12px", overflow: "hidden"}}>
|
<div className="ecap-topology-node" style={{position: "relative", width: "max-content", minWidth: width, maxWidth: "min(944px, calc(100vw - 72px))", boxSizing: "border-box", border: `2px solid ${data.kind?.stroke ?? "#8c8c8c"}`, borderRadius: 12, background: data.kind?.fill ?? "#fff", opacity: node.enabled ? 1 : 0.58, boxShadow: "0 5px 16px rgba(0,0,0,.10)", padding: "11px 12px", overflow: "hidden"}}>
|
||||||
<AmisPanel schema={node_schema} data={node as unknown as Record<string, unknown>}/>
|
<AmisPanel schema={node_schema} data={node as unknown as Record<string, unknown>}/>
|
||||||
<Node_Status node={node} config={data.status} expansion={data.status_expansion}/>
|
<Node_Status node={node} config={data.status} expansion={data.status_expansion}/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user