diff --git a/frontend/src/components/CaseInvestigationView.tsx b/frontend/src/components/CaseInvestigationView.tsx index e9850fd..4302aeb 100644 --- a/frontend/src/components/CaseInvestigationView.tsx +++ b/frontend/src/components/CaseInvestigationView.tsx @@ -63,7 +63,7 @@ export default function CaseInvestigationView({ caseId }: CaseInvestigationViewP } onClick={loadReport}>Retry} /> diff --git a/frontend/src/components/CasePlaybookRunModal.tsx b/frontend/src/components/CasePlaybookRunModal.tsx index 41b0242..62a19e3 100644 --- a/frontend/src/components/CasePlaybookRunModal.tsx +++ b/frontend/src/components/CasePlaybookRunModal.tsx @@ -4,6 +4,7 @@ import {message} from '../utils/appMessage' import {ReloadOutlined, SearchOutlined, ThunderboltOutlined} from '@ant-design/icons' import type {ColumnsType} from 'antd/es/table' import client from '../api/client' +import OverflowTags from './OverflowTags' import {comfortableTagProps} from '../utils/tagStyles' type RecordRow = Record @@ -56,18 +57,7 @@ function normalizeTags(tags: unknown) { } function PlaybookTags({ tags }: { tags: unknown }) { - const normalizedTags = normalizeTags(tags) - if (!normalizedTags.length) return - - return ( - - {normalizedTags.map((tag) => ( - - {tag} - - ))} - - ) + return PLAYBOOK_TAG_COLORS[tag] || 'blue'} /> } function CasePlaybookRunModal({ open, caseId, onClose, onSubmitted }: CasePlaybookRunModalProps) { diff --git a/frontend/src/components/OverflowTags.tsx b/frontend/src/components/OverflowTags.tsx index 9937b71..c98fc29 100644 --- a/frontend/src/components/OverflowTags.tsx +++ b/frontend/src/components/OverflowTags.tsx @@ -1,5 +1,5 @@ import type {CSSProperties} from 'react' -import {useLayoutEffect, useMemo, useRef, useState} from 'react' +import {useMemo} from 'react' import {Tag, Tooltip} from 'antd' import {comfortableTagProps} from '../utils/tagStyles' import {emptyValueNode} from '../utils/recordDisplay' @@ -7,6 +7,8 @@ import {emptyValueNode} from '../utils/recordDisplay' interface OverflowTagsProps { items: unknown color?: string + maxVisible?: number + getColor?: (item: string) => string } const TAG_GAP = 4 @@ -39,109 +41,36 @@ const containerStyle: CSSProperties = { whiteSpace: 'nowrap', verticalAlign: 'middle', } -const measureStyle: CSSProperties = { - position: 'absolute', - left: 0, - top: 0, - display: 'inline-flex', - alignItems: 'center', - gap: TAG_GAP, - visibility: 'hidden', - pointerEvents: 'none', - whiteSpace: 'nowrap', -} -function totalWidth(widths: number[]) { - if (!widths.length) return 0 - return widths.reduce((total, width) => total + width, 0) + TAG_GAP * (widths.length - 1) -} - -function visibleTagCount(values: string[], availableWidth: number, tagWidths: number[], indicatorWidths: Map) { - if (!values.length || availableWidth <= 0) return values.length - if (totalWidth(tagWidths) <= availableWidth) return values.length - - for (let count = values.length - 1; count >= 0; count -= 1) { - const hiddenCount = values.length - count - const indicatorWidth = indicatorWidths.get(hiddenCount) ?? 0 - const widths = count > 0 ? [...tagWidths.slice(0, count), indicatorWidth] : [indicatorWidth] - if (totalWidth(widths) <= availableWidth) return count - } - - return 0 -} - -export default function OverflowTags({ items, color = 'blue' }: OverflowTagsProps) { +export default function OverflowTags({ items, color = 'blue', maxVisible = 2, getColor }: OverflowTagsProps) { const values = useMemo(() => Array.isArray(items) ? items.map((item) => String(item)) : [], [items]) - const containerRef = useRef(null) - const measureRef = useRef(null) - const [visibleCount, setVisibleCount] = useState(values.length) - - useLayoutEffect(() => { - const updateVisibleCount = () => { - const container = containerRef.current - const measure = measureRef.current - if (!container || !measure) return - - const availableWidth = container.getBoundingClientRect().width - const tagWidths = Array.from(measure.querySelectorAll('[data-tag-index]')) - .sort((left, right) => Number(left.dataset.tagIndex) - Number(right.dataset.tagIndex)) - .map((node) => node.getBoundingClientRect().width) - const indicatorWidths = new Map() - measure.querySelectorAll('[data-hidden-count]').forEach((node) => { - indicatorWidths.set(Number(node.dataset.hiddenCount), node.getBoundingClientRect().width) - }) - - const nextVisibleCount = visibleTagCount(values, availableWidth, tagWidths, indicatorWidths) - setVisibleCount((previous) => previous === nextVisibleCount ? previous : nextVisibleCount) - } - - updateVisibleCount() - const container = containerRef.current - if (!container) return undefined - - const resizeObserver = new ResizeObserver(updateVisibleCount) - resizeObserver.observe(container) - return () => resizeObserver.disconnect() - }, [values]) if (!values.length) { return emptyValueNode() } - const safeVisibleCount = Math.min(visibleCount, values.length) + const safeVisibleCount = Math.min(Math.max(maxVisible, 0), values.length) const visible = values.slice(0, safeVisibleCount) const hidden = values.slice(safeVisibleCount) + const colorFor = (item: string) => getColor?.(item) || color const hiddenTitle = ( {hidden.map((item, index) => ( - {item} + {item} ))} ) return ( - + {visible.map((item, index) => ( - {item} + {item} ))} {hidden.length > 0 && ( +{hidden.length} )} - ) } diff --git a/frontend/src/pages/CustomDefinitions.tsx b/frontend/src/pages/CustomDefinitions.tsx index 3954107..ece92e1 100644 --- a/frontend/src/pages/CustomDefinitions.tsx +++ b/frontend/src/pages/CustomDefinitions.tsx @@ -7,6 +7,7 @@ import {Boxes, BrainCircuit, DatabaseZap} from 'lucide-react' import client from '../api/client' import JsonViewer from '../components/JsonViewer' import IconTabLabel from '../components/IconTabLabel' +import OverflowTags from '../components/OverflowTags' import {comfortableTagProps} from '../utils/tagStyles' type SourceType = 'official' | 'custom' @@ -436,11 +437,7 @@ function PlaybooksTab() { title: 'Tags', dataIndex: 'tags', width: 260, - render: (tags: string[]) => ( - - {(tags || []).map((tag) => {tag})} - - ), + render: (tags: string[]) => PLAYBOOK_TAG_COLORS[tag] || 'blue'} />, }, { title: 'Description', dataIndex: 'description', ellipsis: true }, { title: 'Path', dataIndex: 'path', width: 300, render: (path: string) => },