Loading analyst/src/components/CauseAndEffectTree/Link.tsx 0 → 100644 +28 −0 Original line number Diff line number Diff line import { Colors } from '@blueprintjs/colors' import type { HierarchyLink } from 'd3-hierarchy' import type { FC } from 'react' import type { TreeNode } from '../dataHooks/useActionLogsRoot' import { NODE_WIDTH } from './utils' interface LinkProps { link: HierarchyLink<TreeNode> } export const Link: FC<LinkProps> = ({ link }) => { const x1 = link.source.x const y1 = link.source.y! + NODE_WIDTH / 2 const x2 = link.target.x const y2 = link.target.y! - NODE_WIDTH / 2 const midY = (y1 + y2) / 2 const d = `M${y1},${x1}C${midY},${x1} ${midY},${x2} ${y2},${x2}` return ( <path d={d} className='link' fill='none' stroke={Colors.GRAY3} strokeWidth={2} /> ) } analyst/src/components/CauseAndEffectTree/Node.tsx 0 → 100644 +73 −0 Original line number Diff line number Diff line import { Colors } from '@blueprintjs/colors' import { Classes } from '@blueprintjs/core' import { css } from '@emotion/css' import type { HierarchyNode } from 'd3-hierarchy' import type { FC } from 'react' import type { TreeNode } from '../dataHooks/useActionLogsRoot' import { NodeDetail } from './NodeDetail' import { NodeTitle } from './NodeTitle' import { NODE_WIDTH } from './utils' const TEXT_PADDING = 4 const MIN_HEIGHT = 60 const DETAIL_HEIGHT = 24 const internalNode = css` width: 100%; height: 100%; display: flex; flex-direction: column; gap: 0.25rem; align-items: center; text-align: center; justify-content: center; word-break: break-word; padding: 0.25rem; ` const rectStyles = css` fill: ${Colors.LIGHT_GRAY4}; stroke: ${Colors.GRAY3}; .${Classes.DARK} & { fill: ${Colors.DARK_GRAY4}; } ` interface NodeProps { node: HierarchyNode<TreeNode> nodeIndex: number } export const Node: FC<NodeProps> = ({ node, nodeIndex }) => { const hasDetail = typeof node.data.data !== 'string' const totalHeight = MIN_HEIGHT + (hasDetail ? DETAIL_HEIGHT : 0) return ( <g key={`node-${nodeIndex}`} className='node' transform={`translate(${node.y},${node.x})`} > <rect width={NODE_WIDTH} height={totalHeight} x={-NODE_WIDTH / 2} y={-totalHeight / 2} rx={4} className={rectStyles} /> <foreignObject width={NODE_WIDTH - TEXT_PADDING * 2} height={totalHeight} x={-NODE_WIDTH / 2 + TEXT_PADDING} y={-totalHeight / 2} > <div className={internalNode}> <NodeTitle nodeData={node.data.data} /> <NodeDetail nodeData={node.data.data} /> </div> </foreignObject> </g> ) } analyst/src/components/CauseAndEffectTree/NodeDetail.tsx 0 → 100644 +27 −0 Original line number Diff line number Diff line import { css } from '@emotion/css' import type { FC } from 'react' import { ActionLogIcon } from '../ActionLogTitle/ActionLogIcon' import type { TreeNode } from '../dataHooks/useActionLogsRoot' const actionLogDetail = css` display: flex; align-items: center; justify-content: center; gap: 0.25rem; ` interface NodeDetailProps { nodeData: TreeNode['data'] } export const NodeDetail: FC<NodeDetailProps> = ({ nodeData }) => { if (typeof nodeData === 'string') { return null } return ( <div className={actionLogDetail}> <ActionLogIcon actionLog={nodeData} /> <div>{nodeData.logType}</div> </div> ) } analyst/src/components/CauseAndEffectTree/NodeTitle.tsx 0 → 100644 +28 −0 Original line number Diff line number Diff line import { css } from '@emotion/css' import type { FC } from 'react' import { ActionLogContent } from '../ActionLogTitle/ActionLogContent' import type { TreeNode } from '../dataHooks/useActionLogsRoot' // ensure ellipsis for long titles and prevent layout breaking const titleStyles = css` display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; ` interface NodeTitleProps { nodeData: TreeNode['data'] } export const NodeTitle: FC<NodeTitleProps> = ({ nodeData }) => { if (typeof nodeData === 'string') { return <div className={titleStyles}>{nodeData}</div> } return ( <div className={titleStyles}> <ActionLogContent actionLog={nodeData} /> </div> ) } analyst/src/components/CauseAndEffectTree/index.tsx +13 −9 Original line number Diff line number Diff line import type { VariablesOf } from '@inject/graphql' import { AnalystActionLogsSimpleSubscription, useSubscription, } from '@inject/graphql' import { hierarchy, tree } from 'd3-hierarchy' import { select } from 'd3-selection' import type { D3ZoomEvent, ZoomBehavior } from 'd3-zoom' import { zoom, zoomIdentity } from 'd3-zoom' import type { FC } from 'react' import { useEffect, useRef } from 'react' import type { VariablesOf } from '@inject/graphql' import { AnalystActionLogsSimpleSubscription, useSubscription, } from '@inject/graphql' import type { TreeNode } from '../dataHooks/useActionLogsRoot' import useActionLogsRoot from '../dataHooks/useActionLogsRoot' import { renderLinks, renderNodes } from './utils' import { Link } from './Link' import { Node } from './Node' type CauseAndEffectTreeProps = { teamId: string Loading Loading @@ -74,8 +74,12 @@ export const CauseAndEffectTree: FC<CauseAndEffectTreeProps> = ({ teamId }) => { style={{ width: '100%', height: '100%', display: 'block' }} > <g className='zoom-container' ref={gRef}> {renderLinks(links)} {renderNodes(nodes)} {links.map((link, i) => ( <Link key={`link-${i}`} link={link} /> ))} {nodes.map((node, nodeIndex) => ( <Node key={`node-${nodeIndex}`} node={node} nodeIndex={nodeIndex} /> ))} </g> </svg> ) Loading Loading
analyst/src/components/CauseAndEffectTree/Link.tsx 0 → 100644 +28 −0 Original line number Diff line number Diff line import { Colors } from '@blueprintjs/colors' import type { HierarchyLink } from 'd3-hierarchy' import type { FC } from 'react' import type { TreeNode } from '../dataHooks/useActionLogsRoot' import { NODE_WIDTH } from './utils' interface LinkProps { link: HierarchyLink<TreeNode> } export const Link: FC<LinkProps> = ({ link }) => { const x1 = link.source.x const y1 = link.source.y! + NODE_WIDTH / 2 const x2 = link.target.x const y2 = link.target.y! - NODE_WIDTH / 2 const midY = (y1 + y2) / 2 const d = `M${y1},${x1}C${midY},${x1} ${midY},${x2} ${y2},${x2}` return ( <path d={d} className='link' fill='none' stroke={Colors.GRAY3} strokeWidth={2} /> ) }
analyst/src/components/CauseAndEffectTree/Node.tsx 0 → 100644 +73 −0 Original line number Diff line number Diff line import { Colors } from '@blueprintjs/colors' import { Classes } from '@blueprintjs/core' import { css } from '@emotion/css' import type { HierarchyNode } from 'd3-hierarchy' import type { FC } from 'react' import type { TreeNode } from '../dataHooks/useActionLogsRoot' import { NodeDetail } from './NodeDetail' import { NodeTitle } from './NodeTitle' import { NODE_WIDTH } from './utils' const TEXT_PADDING = 4 const MIN_HEIGHT = 60 const DETAIL_HEIGHT = 24 const internalNode = css` width: 100%; height: 100%; display: flex; flex-direction: column; gap: 0.25rem; align-items: center; text-align: center; justify-content: center; word-break: break-word; padding: 0.25rem; ` const rectStyles = css` fill: ${Colors.LIGHT_GRAY4}; stroke: ${Colors.GRAY3}; .${Classes.DARK} & { fill: ${Colors.DARK_GRAY4}; } ` interface NodeProps { node: HierarchyNode<TreeNode> nodeIndex: number } export const Node: FC<NodeProps> = ({ node, nodeIndex }) => { const hasDetail = typeof node.data.data !== 'string' const totalHeight = MIN_HEIGHT + (hasDetail ? DETAIL_HEIGHT : 0) return ( <g key={`node-${nodeIndex}`} className='node' transform={`translate(${node.y},${node.x})`} > <rect width={NODE_WIDTH} height={totalHeight} x={-NODE_WIDTH / 2} y={-totalHeight / 2} rx={4} className={rectStyles} /> <foreignObject width={NODE_WIDTH - TEXT_PADDING * 2} height={totalHeight} x={-NODE_WIDTH / 2 + TEXT_PADDING} y={-totalHeight / 2} > <div className={internalNode}> <NodeTitle nodeData={node.data.data} /> <NodeDetail nodeData={node.data.data} /> </div> </foreignObject> </g> ) }
analyst/src/components/CauseAndEffectTree/NodeDetail.tsx 0 → 100644 +27 −0 Original line number Diff line number Diff line import { css } from '@emotion/css' import type { FC } from 'react' import { ActionLogIcon } from '../ActionLogTitle/ActionLogIcon' import type { TreeNode } from '../dataHooks/useActionLogsRoot' const actionLogDetail = css` display: flex; align-items: center; justify-content: center; gap: 0.25rem; ` interface NodeDetailProps { nodeData: TreeNode['data'] } export const NodeDetail: FC<NodeDetailProps> = ({ nodeData }) => { if (typeof nodeData === 'string') { return null } return ( <div className={actionLogDetail}> <ActionLogIcon actionLog={nodeData} /> <div>{nodeData.logType}</div> </div> ) }
analyst/src/components/CauseAndEffectTree/NodeTitle.tsx 0 → 100644 +28 −0 Original line number Diff line number Diff line import { css } from '@emotion/css' import type { FC } from 'react' import { ActionLogContent } from '../ActionLogTitle/ActionLogContent' import type { TreeNode } from '../dataHooks/useActionLogsRoot' // ensure ellipsis for long titles and prevent layout breaking const titleStyles = css` display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; ` interface NodeTitleProps { nodeData: TreeNode['data'] } export const NodeTitle: FC<NodeTitleProps> = ({ nodeData }) => { if (typeof nodeData === 'string') { return <div className={titleStyles}>{nodeData}</div> } return ( <div className={titleStyles}> <ActionLogContent actionLog={nodeData} /> </div> ) }
analyst/src/components/CauseAndEffectTree/index.tsx +13 −9 Original line number Diff line number Diff line import type { VariablesOf } from '@inject/graphql' import { AnalystActionLogsSimpleSubscription, useSubscription, } from '@inject/graphql' import { hierarchy, tree } from 'd3-hierarchy' import { select } from 'd3-selection' import type { D3ZoomEvent, ZoomBehavior } from 'd3-zoom' import { zoom, zoomIdentity } from 'd3-zoom' import type { FC } from 'react' import { useEffect, useRef } from 'react' import type { VariablesOf } from '@inject/graphql' import { AnalystActionLogsSimpleSubscription, useSubscription, } from '@inject/graphql' import type { TreeNode } from '../dataHooks/useActionLogsRoot' import useActionLogsRoot from '../dataHooks/useActionLogsRoot' import { renderLinks, renderNodes } from './utils' import { Link } from './Link' import { Node } from './Node' type CauseAndEffectTreeProps = { teamId: string Loading Loading @@ -74,8 +74,12 @@ export const CauseAndEffectTree: FC<CauseAndEffectTreeProps> = ({ teamId }) => { style={{ width: '100%', height: '100%', display: 'block' }} > <g className='zoom-container' ref={gRef}> {renderLinks(links)} {renderNodes(nodes)} {links.map((link, i) => ( <Link key={`link-${i}`} link={link} /> ))} {nodes.map((node, nodeIndex) => ( <Node key={`node-${nodeIndex}`} node={node} nodeIndex={nodeIndex} /> ))} </g> </svg> ) Loading