farmcontrol-ui/src/components/Dashboard/common/DashboardOverviewPage.jsx
Tom Butcher 24a3a03c65
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Enhance Dashboard Functionality and Icon Integration
- Updated DashboardLayout to utilize Electron context for dynamic spacing adjustments.
- Enhanced About and Settings components to include icon keys for improved navigation clarity.
- Refined DashboardOverviewPage to generate dynamic icon keys based on page names.
- Improved DashboardTabs to support sidebar icons, enhancing visual consistency.
- Expanded NavigationTabsContext to manage icon keys for tabs, streamlining tab interactions.
- Updated sidebarIconMap to utilize component references for better performance and maintainability.
2026-09-19 01:57:42 +01:00

1023 lines
31 KiB
JavaScript

import {
cloneElement,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { Flex, Space } from 'antd'
import PropTypes from 'prop-types'
import { HolderOutlined } from '@ant-design/icons'
import ActionsButton from './ActionsButton'
import ViewButton from './ViewButton'
import EditButtons from './EditButtons'
import ScrollBox from './ScrollBox'
import InfoCollapse from './InfoCollapse'
import StatsDisplay, { StatsViewButton } from './StatsDisplay'
import ModelHistoryDisplay from './ModelHistoryDisplay'
import LazyWhenVisible from './LazyWhenVisible'
import ObjectTable from './ObjectTable'
import DashboardOverviewFlexColumns from './DashboardOverviewFlexColumns'
import useCollapseState from '../hooks/useCollapseState'
import usePageLayout, {
mergeOrder,
normalizeColumnProportions
} from '../hooks/usePageLayout'
import { OverviewLayoutContext } from '../context/OverviewLayoutContext'
import { useNavigationTabPage } from '../context/NavigationTabsContext'
const flattenOverviewSections = (sectionList = []) => {
const leaves = {}
const defaultFlexColumns = {}
const defaultSectionOrder = []
for (const section of sectionList) {
if (section?.type === 'row') {
const childKeys = []
for (const column of section.columns || []) {
for (const item of column) {
if (item?.key) {
leaves[item.key] = item
childKeys.push(item.key)
}
}
}
defaultFlexColumns[section.key] = childKeys
defaultSectionOrder.push(section.key)
} else if (section?.key) {
leaves[section.key] = section
defaultSectionOrder.push(section.key)
}
}
return { leaves, defaultSectionOrder, defaultFlexColumns }
}
const collectViewItems = (sectionList = []) => {
const items = []
const visit = (section) => {
if (section?.type === 'row') {
section.columns?.forEach((column) => column.forEach(visit))
return
}
if (section?.key && section?.title) {
items.push({ key: section.key, label: section.title })
}
}
sectionList.forEach(visit)
return items
}
const copyLayout = (layout) => ({
sectionOrder: [...(layout?.sectionOrder || [])],
flexColumns: Object.fromEntries(
Object.entries(layout?.flexColumns || {}).map(([key, value]) => [
key,
Array.isArray(value) ? [...value] : []
])
),
statsOrder: Object.fromEntries(
Object.entries(layout?.statsOrder || {}).map(([key, value]) => [
key,
Array.isArray(value) ? [...value] : value
])
),
statsVisibility: Object.fromEntries(
Object.entries(layout?.statsVisibility || {}).map(([key, value]) => [
key,
value && typeof value === 'object' && !Array.isArray(value)
? { ...value }
: {}
])
),
sectionHeights: { ...(layout?.sectionHeights || {}) },
flexColumnProportions: Object.fromEntries(
Object.entries(layout?.flexColumnProportions || {}).map(([key, value]) => [
key,
Array.isArray(value) ? [...value] : []
])
)
})
const createFlexColumnsId = () =>
`flexColumns-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const DEFAULT_HISTORY_HEIGHT = 400
const DEFAULT_TABLE_HEIGHT = 360
const EMPTY_ORDER = []
const EMPTY_COLUMNS = {}
const getVerticalEdgeInsert = (element, clientY) => {
const rect = element.getBoundingClientRect()
const edge = Math.min(48, rect.height / 3)
const y = clientY - rect.top
if (y <= edge) return { insertBefore: true }
if (y >= rect.height - edge) return { insertBefore: false }
return null
}
const DashboardOverviewPage = ({
pageName,
collapseDefaults = {},
viewItems,
actionMenu,
onOpenActionsModal,
sections = []
}) => {
const overviewSection = pageName.endsWith('Overview')
? pageName.slice(0, -'Overview'.length)
: ''
const overviewTitle = overviewSection
? `Overview - ${overviewSection}`
: pageName
const overviewIconKey = overviewSection
? `${overviewSection.charAt(0).toLowerCase()}${overviewSection.slice(1)}`
: undefined
useNavigationTabPage({ title: overviewTitle, iconKey: overviewIconKey })
const [savedCollapseState, updateCollapseState] = useCollapseState(
pageName,
collapseDefaults
)
const collapseState = useMemo(
() => ({ ...collapseDefaults, ...savedCollapseState }),
[collapseDefaults, savedCollapseState]
)
const { leaves, defaultSectionOrder, defaultFlexColumns } = useMemo(
() => flattenOverviewSections(sections),
[sections]
)
const [savedLayout, updatePageLayout] = usePageLayout(pageName, {
sectionOrder: defaultSectionOrder,
flexColumns: defaultFlexColumns,
statsOrder: {},
statsVisibility: {}
})
const [isLayoutEditing, setIsLayoutEditing] = useState(false)
const [draftLayout, setDraftLayout] = useState(null)
const [draggedItem, setDraggedItem] = useState(null)
const [dropTarget, setDropTarget] = useState(null)
const [liveHeights, setLiveHeights] = useState({})
const scrollRef = useRef(null)
const dragYRef = useRef(null)
const scrollRafRef = useRef(0)
const displayLayout =
isLayoutEditing && draftLayout ? draftLayout : savedLayout
const resolvedViewItems = viewItems || collectViewItems(sections)
const sectionsByKey = leaves
const orderedSectionKeys = useMemo(
() => displayLayout.sectionOrder || EMPTY_ORDER,
[displayLayout.sectionOrder]
)
const flexColumns = useMemo(
() => displayLayout.flexColumns || EMPTY_COLUMNS,
[displayLayout.flexColumns]
)
const isGroupKey = (key) =>
Object.prototype.hasOwnProperty.call(flexColumns, key)
const startEditing = useCallback(() => {
setDraftLayout(copyLayout(savedLayout))
setIsLayoutEditing(true)
}, [savedLayout])
const clearDragState = useCallback(() => {
setDraggedItem(null)
setDropTarget(null)
}, [])
const getSectionHeight = useCallback(
(key, fallback) => {
if (liveHeights[key] != null) return liveHeights[key]
const saved = savedLayout.sectionHeights?.[key]
return typeof saved === 'number' ? saved : fallback
},
[liveHeights, savedLayout.sectionHeights]
)
const handleSectionHeightChange = useCallback(
(key, height, persist) => {
setLiveHeights((current) => ({ ...current, [key]: height }))
if (!persist) return
const nextHeights = {
...(savedLayout.sectionHeights || {}),
[key]: height
}
setDraftLayout((current) => {
if (!current) return current
return { ...current, sectionHeights: nextHeights }
})
updatePageLayout({
...savedLayout,
sectionHeights: nextHeights
})
},
[savedLayout, updatePageLayout]
)
useEffect(() => {
if (!draggedItem) return undefined
const edgePx = 72
const maxSpeed = 28
const stopScroll = () => {
if (scrollRafRef.current) {
cancelAnimationFrame(scrollRafRef.current)
scrollRafRef.current = 0
}
}
const tick = () => {
scrollRafRef.current = 0
const y = dragYRef.current
const scrollEl = scrollRef.current
if (y == null || !scrollEl) return
const rect = scrollEl.getBoundingClientRect()
let dy = 0
if (y >= rect.bottom - edgePx) {
dy = Math.min(maxSpeed, Math.ceil((y - (rect.bottom - edgePx)) / 2) + 6)
} else if (y <= rect.top + edgePx) {
dy = -Math.min(maxSpeed, Math.ceil((rect.top + edgePx - y) / 2) + 6)
}
if (dy === 0) return
const maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight
const next = Math.max(0, Math.min(maxScroll, scrollEl.scrollTop + dy))
if (next !== scrollEl.scrollTop) {
scrollEl.scrollTop = next
scrollRafRef.current = requestAnimationFrame(tick)
}
}
const onDragOver = (event) => {
dragYRef.current = event.clientY
if (!scrollRafRef.current) {
scrollRafRef.current = requestAnimationFrame(tick)
}
}
const onDragEnd = () => {
dragYRef.current = null
stopScroll()
}
document.addEventListener('dragover', onDragOver)
document.addEventListener('dragend', onDragEnd)
document.addEventListener('drop', onDragEnd)
return () => {
document.removeEventListener('dragover', onDragOver)
document.removeEventListener('dragend', onDragEnd)
document.removeEventListener('drop', onDragEnd)
onDragEnd()
}
}, [draggedItem])
const cancelEditing = useCallback(() => {
setDraftLayout(null)
setIsLayoutEditing(false)
clearDragState()
}, [clearDragState])
const handleUpdate = useCallback(() => {
if (draftLayout) {
updatePageLayout(draftLayout)
}
setDraftLayout(null)
setIsLayoutEditing(false)
clearDragState()
}, [draftLayout, updatePageLayout, clearDragState])
const reorderStats = useCallback(
(objectType, newOrder) => {
if (!isLayoutEditing) return
setDraftLayout((current) => {
const base = current || copyLayout(savedLayout)
return {
...base,
statsOrder: {
...base.statsOrder,
[objectType]: newOrder
}
}
})
},
[isLayoutEditing, savedLayout]
)
const updateStatsVisibility = useCallback(
(objectType, key, value) => {
if (!isLayoutEditing) return
setDraftLayout((current) => {
const base = current || copyLayout(savedLayout)
return {
...base,
statsVisibility: {
...base.statsVisibility,
[objectType]: {
...(base.statsVisibility?.[objectType] || {}),
[key]: value
}
}
}
})
},
[isLayoutEditing, savedLayout]
)
const layoutContextValue = useMemo(
() => ({
isLayoutEditing,
getStatsOrder: (objectType, defaultOrder = []) =>
mergeOrder(displayLayout.statsOrder?.[objectType] || [], defaultOrder),
reorderStats,
getStatsVisibility: (objectType, defaultNames = []) => {
const saved = displayLayout.statsVisibility?.[objectType] || {}
return Object.fromEntries(
defaultNames.map((name) => [name, saved[name] !== false])
)
},
updateStatsVisibility
}),
[isLayoutEditing, displayLayout, reorderStats, updateStatsVisibility]
)
const applyMove = useCallback(
(dest) => {
if (!draggedItem || !dest) return
if (draggedItem.kind === 'flexGroup' && dest.parent !== 'root') return
setDraftLayout((current) => {
const base = current || copyLayout(savedLayout)
const originalSourceList =
draggedItem.sourceParent === 'root'
? base.sectionOrder
: base.flexColumns?.[draggedItem.sourceParent] || []
const sourceIndex = originalSourceList.indexOf(draggedItem.key)
if (sourceIndex === -1) return current || base
const originalDestList =
dest.parent === 'root'
? base.sectionOrder
: base.flexColumns?.[dest.parent] || []
let destIndex = dest.insertBefore ? dest.index : dest.index + 1
if (
draggedItem.sourceParent === dest.parent &&
sourceIndex < destIndex
) {
destIndex -= 1
}
destIndex = Math.max(0, Math.min(destIndex, originalDestList.length))
if (
draggedItem.sourceParent === dest.parent &&
destIndex === sourceIndex
) {
return current || base
}
const sectionOrder = [...(base.sectionOrder || [])]
const nextFlexColumns = Object.fromEntries(
Object.entries(base.flexColumns || {}).map(([id, items]) => [
id,
[...items]
])
)
if (draggedItem.sourceParent === 'root') {
const from = sectionOrder.indexOf(draggedItem.key)
if (from !== -1) sectionOrder.splice(from, 1)
} else {
nextFlexColumns[draggedItem.sourceParent] = (
nextFlexColumns[draggedItem.sourceParent] || []
).filter((key) => key !== draggedItem.key)
}
if (dest.parent === 'root') {
const insertAt = Math.max(0, Math.min(destIndex, sectionOrder.length))
sectionOrder.splice(insertAt, 0, draggedItem.key)
} else {
const destList = [...(nextFlexColumns[dest.parent] || [])]
const insertAt = Math.max(0, Math.min(destIndex, destList.length))
destList.splice(insertAt, 0, draggedItem.key)
nextFlexColumns[dest.parent] = destList
}
return {
...base,
sectionOrder,
flexColumns: nextFlexColumns,
flexColumnProportions: Object.fromEntries(
Object.entries(nextFlexColumns).map(([id, items]) => [
id,
normalizeColumnProportions(
items.length,
base.flexColumnProportions?.[id]
)
])
)
}
})
},
[draggedItem, savedLayout]
)
const handleDragStart = useCallback(
(e, item) => {
if (!isLayoutEditing) return
e.stopPropagation()
const selector =
item.sourceParent === 'root'
? '.overview-section-wrapper'
: '.overview-flex-columns-item'
const wrapper = e.currentTarget.closest(selector)
if (wrapper) {
const rect = wrapper.getBoundingClientRect()
e.dataTransfer.setDragImage(
wrapper,
e.clientX - rect.left,
e.clientY - rect.top
)
}
setDraggedItem(item)
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/html', '')
},
[isLayoutEditing]
)
const setRootDropFromGroupEdge = useCallback(
(e, groupId) => {
const wrapper = e.currentTarget.closest('.overview-section-wrapper')
if (!wrapper) return false
const edge = getVerticalEdgeInsert(wrapper, e.clientY)
if (!edge) return false
const rootIndex = orderedSectionKeys.indexOf(groupId)
if (rootIndex === -1) return false
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
setDropTarget({
parent: 'root',
index: rootIndex,
insertBefore: edge.insertBefore
})
return true
},
[orderedSectionKeys]
)
const handleRootDragOver = useCallback(
(e, index, key) => {
if (!isLayoutEditing || !draggedItem) return
const isGroup = Object.prototype.hasOwnProperty.call(flexColumns, key)
if (draggedItem.kind === 'section' && isGroup) {
const edge = getVerticalEdgeInsert(e.currentTarget, e.clientY)
if (edge) {
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
setDropTarget({
parent: 'root',
index,
insertBefore: edge.insertBefore
})
}
return
}
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
const rect = e.currentTarget.getBoundingClientRect()
setDropTarget({
parent: 'root',
index,
insertBefore: e.clientY < rect.top + rect.height / 2
})
},
[isLayoutEditing, draggedItem, flexColumns]
)
const handleRootListDragOver = useCallback(
(e) => {
if (!isLayoutEditing || !draggedItem) return
if (
e.target instanceof Element &&
e.target.closest('.overview-section-wrapper')
) {
return
}
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
const wrappers = [...e.currentTarget.children].filter((el) =>
el.classList.contains('overview-section-wrapper')
)
if (wrappers.length === 0) {
setDropTarget({ parent: 'root', index: 0, insertBefore: true })
return
}
const y = e.clientY
for (let i = 0; i < wrappers.length; i++) {
const rect = wrappers[i].getBoundingClientRect()
if (y < rect.top + rect.height / 2) {
const key = wrappers[i].dataset.overviewKey
const nextIndex = orderedSectionKeys.indexOf(key)
setDropTarget({
parent: 'root',
index: nextIndex === -1 ? i : nextIndex,
insertBefore: true
})
return
}
}
const lastKey = wrappers[wrappers.length - 1].dataset.overviewKey
const lastIndex = orderedSectionKeys.indexOf(lastKey)
setDropTarget({
parent: 'root',
index: lastIndex === -1 ? wrappers.length - 1 : lastIndex,
insertBefore: false
})
},
[isLayoutEditing, draggedItem, orderedSectionKeys]
)
const handleGroupItemDragOver = useCallback(
(e, groupId, index) => {
if (!isLayoutEditing || !draggedItem || draggedItem.kind !== 'section') {
return
}
if (setRootDropFromGroupEdge(e, groupId)) return
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
setDropTarget({
parent: groupId,
index,
insertBefore: true
})
},
[isLayoutEditing, draggedItem, setRootDropFromGroupEdge]
)
const handleGroupContainerDragOver = useCallback(
(e, groupId) => {
if (!isLayoutEditing || !draggedItem || draggedItem.kind !== 'section') {
return
}
if (setRootDropFromGroupEdge(e, groupId)) return
if (
e.target instanceof Element &&
e.target.closest('.overview-flex-columns-item')
) {
return
}
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
const items = flexColumns[groupId] || []
setDropTarget({
parent: groupId,
index: items.length,
insertBefore: true
})
},
[isLayoutEditing, draggedItem, flexColumns, setRootDropFromGroupEdge]
)
const handleDragLeave = useCallback((e) => {
if (
e.relatedTarget instanceof Node &&
e.currentTarget.contains(e.relatedTarget)
) {
return
}
setDropTarget(null)
}, [])
const handleDrop = useCallback(
(e, fallbackParent, fallbackIndex) => {
if (!isLayoutEditing || !draggedItem) return
e.preventDefault()
e.stopPropagation()
const dest = dropTarget || {
parent: fallbackParent,
index: fallbackIndex,
insertBefore: true
}
applyMove(dest)
clearDragState()
},
[isLayoutEditing, draggedItem, dropTarget, applyMove, clearDragState]
)
const handleDragEnd = useCallback(() => {
clearDragState()
}, [clearDragState])
const addColumns = useCallback(() => {
setIsLayoutEditing(true)
setDraftLayout((current) => {
const base = current || copyLayout(savedLayout)
const id = createFlexColumnsId()
return {
...base,
sectionOrder: [...(base.sectionOrder || []), id],
flexColumns: {
...(base.flexColumns || {}),
[id]: []
}
}
})
}, [savedLayout])
const removeGroup = useCallback(
(groupId) => {
setDraftLayout((current) => {
const base = current || copyLayout(savedLayout)
const children = base.flexColumns?.[groupId] || []
const sectionOrder = [...(base.sectionOrder || [])]
const at = sectionOrder.indexOf(groupId)
if (at === -1) return current || base
sectionOrder.splice(at, 1, ...children)
const nextFlexColumns = { ...(base.flexColumns || {}) }
delete nextFlexColumns[groupId]
const nextFlexColumnProportions = {
...(base.flexColumnProportions || {})
}
delete nextFlexColumnProportions[groupId]
return {
...base,
sectionOrder,
flexColumns: nextFlexColumns,
flexColumnProportions: nextFlexColumnProportions
}
})
},
[savedLayout]
)
const updateFlexColumnProportions = useCallback(
(groupId, nextProportions) => {
if (!isLayoutEditing) return
setDraftLayout((current) => {
const base = current || copyLayout(savedLayout)
return {
...base,
flexColumnProportions: {
...(base.flexColumnProportions || {}),
[groupId]: nextProportions
}
}
})
},
[isLayoutEditing, savedLayout]
)
const renderDragHandle = (item) => (
<span
className='overview-drag-handle'
draggable
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onDragStart={(e) => handleDragStart(e, item)}
onDragEnd={handleDragEnd}
>
<HolderOutlined />
</span>
)
const isItemVisible = (key) => isLayoutEditing || collapseState[key] !== false
const wouldMove = (item, dest) => {
if (!item || !dest) return false
if (item.key === orderedSectionKeys[dest.index] && dest.parent === 'root') {
const insertIndex = dest.insertBefore ? dest.index : dest.index + 1
let targetIndex = insertIndex
if (item.sourceParent === 'root' && item.sourceIndex < targetIndex) {
targetIndex -= 1
}
return targetIndex !== item.sourceIndex
}
if (item.sourceParent === dest.parent) {
const insertIndex = dest.insertBefore ? dest.index : dest.index + 1
let targetIndex = insertIndex
if (item.sourceIndex < targetIndex) {
targetIndex -= 1
}
return targetIndex !== item.sourceIndex
}
return true
}
const getRootWrapperClass = (index, key) => {
const isDragging =
draggedItem?.sourceParent === 'root' && draggedItem.key === key
let insertClass = ''
if (
dropTarget?.parent === 'root' &&
dropTarget.index === index &&
draggedItem &&
draggedItem.key !== key &&
wouldMove(draggedItem, dropTarget)
) {
insertClass = dropTarget.insertBefore
? 'overview-insert-before'
: 'overview-insert-after'
}
return `overview-section-wrapper ${isDragging ? 'overview-dragging' : ''} ${insertClass}`
}
const getGroupItemClass = (groupId, index, key) => {
const isDragging =
draggedItem?.sourceParent === groupId && draggedItem.key === key
const isDragOver =
dropTarget?.parent === groupId &&
dropTarget.index === index &&
draggedItem?.kind === 'section' &&
draggedItem.key !== key
return `overview-flex-columns-item ${isDragging ? 'overview-dragging' : ''} ${
isDragOver ? 'overview-drag-over' : ''
}`
}
const renderCollapse = (section, dragHandle = null) => {
const isStats = section.content?.type === StatsDisplay
const statsObjectType = isStats ? section.content.props?.objectType : null
const isHistory = section.content?.type === ModelHistoryDisplay
const isTable = section.content?.type === ObjectTable
const isLazyContent = isStats || isHistory || isTable
const resizable = isHistory || isTable
const defaultHeight = isHistory
? DEFAULT_HISTORY_HEIGHT
: DEFAULT_TABLE_HEIGHT
const contentHeight = getSectionHeight(section.key, defaultHeight)
const extra =
isLayoutEditing && statsObjectType ? (
<StatsViewButton objectType={statsObjectType} />
) : null
let content = section.content
if (resizable && content) {
content = isHistory
? cloneElement(content, { height: contentHeight })
: cloneElement(content, { scrollHeight: `${contentHeight}px` })
}
if (isLazyContent && content) {
content = (
<LazyWhenVisible
scrollRootRef={scrollRef}
minHeight={
isHistory || isTable ? contentHeight : 120
}
>
{content}
</LazyWhenVisible>
)
}
return (
<InfoCollapse
title={section.title}
icon={section.icon ?? null}
canCollapse={false}
active={collapseState[section.key] !== false}
onToggle={(isActive) => updateCollapseState(section.key, isActive)}
className={section.className || ''}
collapseKey={section.key}
dragHandle={dragHandle}
extra={extra}
resizable={resizable}
contentHeight={contentHeight}
onContentHeightChange={
resizable
? (height, persist) =>
handleSectionHeightChange(section.key, height, persist)
: undefined
}
>
{content}
</InfoCollapse>
)
}
const actionMenuItems = useMemo(() => {
const layoutItems = [
{
key: 'add-columns',
label: 'Add columns',
onClick: addColumns
}
]
const extraItems = actionMenu?.items || []
if (extraItems.length === 0) {
return layoutItems
}
return [...layoutItems, { type: 'divider' }, ...extraItems]
}, [actionMenu, addColumns])
return (
<OverviewLayoutContext.Provider value={layoutContextValue}>
<Flex
gap='large'
vertical
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify='space-between'>
<Space size='small'>
<ActionsButton
menu={{ items: actionMenuItems }}
onOpenModal={onOpenActionsModal}
disabled={false}
/>
<ViewButton
items={resolvedViewItems}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
</Space>
<Space>
<EditButtons
isEditing={isLayoutEditing}
handleUpdate={handleUpdate}
cancelEditing={cancelEditing}
startEditing={startEditing}
formValid={true}
loading={false}
requirePermission={false}
/>
</Space>
</Flex>
<ScrollBox scrollableNodeProps={{ ref: scrollRef }}>
<Flex
vertical
gap='large'
style={isLayoutEditing ? { paddingTop: 13.5 } : undefined}
onDragOver={handleRootListDragOver}
onDrop={(e) => handleDrop(e, 'root', 0)}
>
{orderedSectionKeys.map((key, index) => {
if (isGroupKey(key)) {
const groupItems = flexColumns[key] || []
const itemsToRender = isLayoutEditing
? groupItems
: groupItems.filter((childKey) => isItemVisible(childKey))
if (itemsToRender.length === 0 && !isLayoutEditing) {
return null
}
const groupProportions = normalizeColumnProportions(
groupItems.length,
displayLayout.flexColumnProportions?.[key]
)
const renderProportions = itemsToRender.map((childKey) => {
const itemIndex = groupItems.indexOf(childKey)
return itemIndex === -1
? undefined
: groupProportions[itemIndex]
})
return (
<div
key={key}
data-overview-key={key}
onDragOver={(e) => handleRootDragOver(e, index, key)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, 'root', index)}
className={getRootWrapperClass(index, key)}
>
<DashboardOverviewFlexColumns
isLayoutEditing={isLayoutEditing}
isDragOver={
dropTarget?.parent === key &&
draggedItem?.kind === 'section' &&
dropTarget.index >= groupItems.length
}
dragHandle={
isLayoutEditing
? renderDragHandle({
kind: 'flexGroup',
key,
sourceParent: 'root',
sourceIndex: index
})
: null
}
onRemove={
isLayoutEditing ? () => removeGroup(key) : undefined
}
onDragOver={(e) => handleGroupContainerDragOver(e, key)}
onDrop={(e) => handleDrop(e, key, groupItems.length)}
onDragLeave={handleDragLeave}
proportions={renderProportions}
onProportionsChange={
isLayoutEditing
? (next) => updateFlexColumnProportions(key, next)
: undefined
}
>
{itemsToRender.map((childKey, childIndex) => {
const section = sectionsByKey[childKey]
if (!section) return null
return (
<div
key={childKey}
onDragOver={(e) =>
handleGroupItemDragOver(e, key, childIndex)
}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, key, childIndex)}
className={getGroupItemClass(
key,
childIndex,
childKey
)}
>
{renderCollapse(
section,
isLayoutEditing
? renderDragHandle({
kind: 'section',
key: childKey,
sourceParent: key,
sourceIndex: childIndex
})
: null
)}
</div>
)
})}
</DashboardOverviewFlexColumns>
</div>
)
}
const section = sectionsByKey[key]
if (!section) return null
if (!isItemVisible(section.key)) {
return null
}
return (
<div
key={key}
data-overview-key={key}
onDragOver={(e) => handleRootDragOver(e, index, key)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, 'root', index)}
className={getRootWrapperClass(index, key)}
>
{renderCollapse(
section,
isLayoutEditing
? renderDragHandle({
kind: 'section',
key,
sourceParent: 'root',
sourceIndex: index
})
: null
)}
</div>
)
})}
</Flex>
</ScrollBox>
</Flex>
</OverviewLayoutContext.Provider>
)
}
const sectionShape = PropTypes.shape({
key: PropTypes.string.isRequired,
title: PropTypes.string,
icon: PropTypes.node,
className: PropTypes.string,
content: PropTypes.node,
type: PropTypes.oneOf(['row']),
columns: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.object))
})
DashboardOverviewPage.propTypes = {
pageName: PropTypes.string.isRequired,
collapseDefaults: PropTypes.object,
viewItems: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string.isRequired,
label: PropTypes.string.isRequired
})
),
actionMenu: PropTypes.object,
onOpenActionsModal: PropTypes.func,
sections: PropTypes.arrayOf(sectionShape)
}
export default DashboardOverviewPage