diff --git a/galasa-ui/src/components/test-runs/TestRunsDetails.tsx b/galasa-ui/src/components/test-runs/TestRunsDetails.tsx index fe293c39..42a93263 100644 --- a/galasa-ui/src/components/test-runs/TestRunsDetails.tsx +++ b/galasa-ui/src/components/test-runs/TestRunsDetails.tsx @@ -8,6 +8,7 @@ import BreadCrumb from '@/components/common/BreadCrumb'; import TestRunsTabs from '@/components/test-runs/TestRunsTabs'; import styles from '@/styles/test-runs/TestRunsPage.module.css'; import { Suspense, useEffect, useMemo, useRef, useState } from 'react'; +import { getHeightOfHeaderAndFooter } from '@/utils/functions/getHeightOfHeaderAndFooter'; import useHistoryBreadCrumbs from '@/hooks/useHistoryBreadCrumbs'; import { useTranslations } from 'next-intl'; import { NotificationType } from '@/utils/types/common'; @@ -45,6 +46,7 @@ export default function TestRunsDetails({ const [notification, setNotification] = useState(null); const [isEditingName, setIsEditingName] = useState(false); const [editedName, setEditedName] = useState(''); + const [maxHeight, setMaxHeight] = useState('68vh'); const inputRef = useRef(null); @@ -58,6 +60,24 @@ export default function TestRunsDetails({ } }, [isEditingName]); + // Calculate and set the dynamic max-height based on header and footer heights + useEffect(() => { + const updateMaxHeight = () => { + const headerFooterHeight = getHeightOfHeaderAndFooter(); + setMaxHeight(`calc(100vh - ${headerFooterHeight}px)`); + }; + + // Initial calculation + updateMaxHeight(); + + // Recalculate on window resize + window.addEventListener('resize', updateMaxHeight); + + return () => { + window.removeEventListener('resize', updateMaxHeight); + }; + }, []); + const handleShare = async () => { try { await navigator.clipboard.writeText(window.location.href); @@ -247,7 +267,7 @@ export default function TestRunsDetails({
-
+
void; @@ -54,15 +54,11 @@ export default function CollapsibleSideBar({ handleEditQueryName }: CollapsibleS const [notification, setNotification] = useState(null); const [isExpanded, setIsExpanded] = useState(false); const [searchTerm, setSearchTerm] = useState(''); + const [maxHeight, setMaxHeight] = useState('68vh'); // State to hold the data of the item currently being dragged for the DragOverlay const [activeQuery, setActiveQuery] = useState(null); - const [sideNavExpandedHeight, setSideNavExpandedHeight] = useState(0); - const [mainContentElement, setMainContentElement] = useState(null); - const SIDE_NAV_MIN_HEIGHT_PIXELS = 700; - const SIDE_NAV_HEIGHT_IF_NOT_RESIZABLE_PIXELS = 850; - // Isolate user-sortable queries from the default query const sortableQueries = useMemo( () => savedQueries.filter((query) => query.createdAt !== defaultQuery.createdAt), @@ -154,50 +150,23 @@ export default function CollapsibleSideBar({ handleEditQueryName }: CollapsibleS return sortableQueries; }, [searchTerm, sortableQueries]); - // Grab the main content element on page load. - useEffect(() => { - setMainContentElement(document.querySelector('.' + testRunsPageStyles.mainContent)); - }, []); - + // Calculate and set the dynamic max-height based on header and footer heights useEffect(() => { - const updateSideNavHeight = () => { - if (mainContentElement) { - // As the mainContent for the test runs details is also flex, we must set this height to a minimum, wait a short while, then set the height of this element to the main content minus an offset. - setSideNavExpandedHeight(SIDE_NAV_MIN_HEIGHT_PIXELS); - setTimeout(() => { - // The .clientHeight seems to need mainContentElement checked inside the setTimeout(). - if (mainContentElement) { - const newHeight = mainContentElement.clientHeight - 50; - setSideNavExpandedHeight(newHeight); - } - }, 0); - } + const updateMaxHeight = () => { + const headerFooterHeight = getHeightOfHeaderAndFooter(); + setMaxHeight(`calc(100vh - ${headerFooterHeight}px)`); }; - // Initial update - updateSideNavHeight(); + // Initial calculation + updateMaxHeight(); - // Add event listener for main content resize. - const resizeObserver = new ResizeObserver((entries) => { - // Check if there's a valid entry. - if (entries[0]) { - updateSideNavHeight(); - } - }); - - if (mainContentElement) { - resizeObserver.observe(mainContentElement); - } else { - setSideNavExpandedHeight(SIDE_NAV_HEIGHT_IF_NOT_RESIZABLE_PIXELS); - } + // Recalculate on window resize + window.addEventListener('resize', updateMaxHeight); - // Cleanup function to remove the event listener when the component unmounts return () => { - if (mainContentElement) { - resizeObserver.unobserve(mainContentElement); - } + window.removeEventListener('resize', updateMaxHeight); }; - }, [mainContentElement]); + }, []); return (
@@ -215,10 +184,9 @@ export default function CollapsibleSideBar({ handleEditQueryName }: CollapsibleS onClick={() => setIsExpanded(!isExpanded)} /> -
+
diff --git a/galasa-ui/src/styles/common/BreadCrumb.module.css b/galasa-ui/src/styles/common/BreadCrumb.module.css index 9a0ec8a6..27d0a3d0 100644 --- a/galasa-ui/src/styles/common/BreadCrumb.module.css +++ b/galasa-ui/src/styles/common/BreadCrumb.module.css @@ -5,5 +5,6 @@ */ .crumbContainer { + height: 84px; padding: 2rem 1rem; } diff --git a/galasa-ui/src/styles/test-runs/TestRunsPage.module.css b/galasa-ui/src/styles/test-runs/TestRunsPage.module.css index 46d1d355..f3c32fd9 100644 --- a/galasa-ui/src/styles/test-runs/TestRunsPage.module.css +++ b/galasa-ui/src/styles/test-runs/TestRunsPage.module.css @@ -20,6 +20,7 @@ flex: 1; min-width: 0; padding: 2.5rem 4rem; + overflow: auto; } @media (max-width: 768px) { @@ -34,10 +35,6 @@ } } -.tabsContainer { - height: 100%; -} - .titleText { font-size: 1rem; margin: 0.5rem 0.5rem; diff --git a/galasa-ui/src/tests/components/test-runs/saved-queries/CollapsibleSideBar.test.tsx b/galasa-ui/src/tests/components/test-runs/saved-queries/CollapsibleSideBar.test.tsx index 6cdd91db..51350dc3 100644 --- a/galasa-ui/src/tests/components/test-runs/saved-queries/CollapsibleSideBar.test.tsx +++ b/galasa-ui/src/tests/components/test-runs/saved-queries/CollapsibleSideBar.test.tsx @@ -352,33 +352,4 @@ describe('CollapsibleSideBar', () => { expect(mockSetSavedQueries).not.toHaveBeenCalled(); }); }); - - describe('updating side nav height', () => { - test('should not observe the main content if main content not loaded', async () => { - render(); - expect(mockObserve).toHaveBeenCalledTimes(0); - }); - - test('should observe the main content if main content rendered, and set to height of main content -50px', async () => { - const mainContentElement = document.createElement('div'); - mainContentElement.className = 'mainContent'; - document.body.appendChild(mainContentElement); - - render(); - - const sidebar = screen.getByLabelText('Saved Queries Sidebar'); - - await waitFor(() => { - expect(mockObserve).toHaveBeenCalledTimes(1); - - if (sidebar) { - expect(sidebar.style.height).toBe('-50px'); - } else { - fail('could not find sidebar'); - } - - document.body.innerHTML = ''; - }); - }); - }); }); diff --git a/galasa-ui/src/tests/utils/functions/getHeightOfHeaderAndFooter.test.ts b/galasa-ui/src/tests/utils/functions/getHeightOfHeaderAndFooter.test.ts new file mode 100644 index 00000000..4ec90ed1 --- /dev/null +++ b/galasa-ui/src/tests/utils/functions/getHeightOfHeaderAndFooter.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import '@testing-library/jest-dom'; +import { describe, expect, test, beforeEach } from '@jest/globals'; +import { getHeightOfHeaderAndFooter } from '@/utils/functions/getHeightOfHeaderAndFooter'; + +describe('getHeightOfHeaderAndFooter', () => { + beforeEach(() => { + // Clear the document body before each test + document.body.innerHTML = ''; + }); + + test('should return 0 when no elements with specified classes exist', () => { + // Given... + // Empty document body + + // When... + const totalHeight = getHeightOfHeaderAndFooter(); + + // Then... + expect(totalHeight).toBe(0); + }); + + test('should return the height of a single element with class "toolbar"', () => { + // Given... + const toolbarElement = document.createElement('div'); + toolbarElement.className = 'toolbar'; + Object.defineProperty(toolbarElement, 'offsetHeight', { + configurable: true, + value: 64, + }); + document.body.appendChild(toolbarElement); + + // When... + const totalHeight = getHeightOfHeaderAndFooter(); + + // Then... + expect(totalHeight).toBe(64); + }); + + test('should accumulate heights from multiple different class elements', () => { + // Given... + const toolbarElement = document.createElement('div'); + toolbarElement.className = 'toolbar'; + Object.defineProperty(toolbarElement, 'offsetHeight', { + configurable: true, + value: 64, + }); + + const crumbElement = document.createElement('div'); + crumbElement.className = 'crumbContainer'; + Object.defineProperty(crumbElement, 'offsetHeight', { + configurable: true, + value: 48, + }); + + const headerElement = document.createElement('div'); + headerElement.className = 'cds--header__global'; + Object.defineProperty(headerElement, 'offsetHeight', { + configurable: true, + value: 48, + }); + + const footerElement = document.createElement('div'); + footerElement.className = 'footer'; + Object.defineProperty(footerElement, 'offsetHeight', { + configurable: true, + value: 80, + }); + + document.body.appendChild(toolbarElement); + document.body.appendChild(crumbElement); + document.body.appendChild(headerElement); + document.body.appendChild(footerElement); + + // When... + const totalHeight = getHeightOfHeaderAndFooter(); + + // Then... + expect(totalHeight).toBe(240); // 64 + 48 + 48 + 80 + }); + + test('should accumulate heights from multiple elements with the same class', () => { + // Given... + const toolbar1 = document.createElement('div'); + toolbar1.className = 'toolbar'; + Object.defineProperty(toolbar1, 'offsetHeight', { + configurable: true, + value: 64, + }); + + const toolbar2 = document.createElement('div'); + toolbar2.className = 'toolbar'; + Object.defineProperty(toolbar2, 'offsetHeight', { + configurable: true, + value: 32, + }); + + document.body.appendChild(toolbar1); + document.body.appendChild(toolbar2); + + // When... + const totalHeight = getHeightOfHeaderAndFooter(); + + // Then... + expect(totalHeight).toBe(96); // 64 + 32 + }); + + test('should handle elements with zero height', () => { + // Given... + const toolbarElement = document.createElement('div'); + toolbarElement.className = 'toolbar'; + Object.defineProperty(toolbarElement, 'offsetHeight', { + configurable: true, + value: 0, + }); + + const footerElement = document.createElement('div'); + footerElement.className = 'footer'; + Object.defineProperty(footerElement, 'offsetHeight', { + configurable: true, + value: 80, + }); + + document.body.appendChild(toolbarElement); + document.body.appendChild(footerElement); + + // When... + const totalHeight = getHeightOfHeaderAndFooter(); + + // Then... + expect(totalHeight).toBe(80); // 0 + 80 + }); + + test('should only count elements with exact class names', () => { + // Given... + const toolbarElement = document.createElement('div'); + toolbarElement.className = 'toolbar'; + Object.defineProperty(toolbarElement, 'offsetHeight', { + configurable: true, + value: 64, + }); + + const notToolbarElement = document.createElement('div'); + notToolbarElement.className = 'toolbar-extra'; + Object.defineProperty(notToolbarElement, 'offsetHeight', { + configurable: true, + value: 100, + }); + + document.body.appendChild(toolbarElement); + document.body.appendChild(notToolbarElement); + + // When... + const totalHeight = getHeightOfHeaderAndFooter(); + + // Then... + expect(totalHeight).toBe(64); // Only the exact 'toolbar' class + }); +}); diff --git a/galasa-ui/src/utils/functions/getHeightOfHeaderAndFooter.ts b/galasa-ui/src/utils/functions/getHeightOfHeaderAndFooter.ts new file mode 100644 index 00000000..c831ac60 --- /dev/null +++ b/galasa-ui/src/utils/functions/getHeightOfHeaderAndFooter.ts @@ -0,0 +1,34 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import breadcrumbStyles from '@/styles/common/BreadCrumb.module.css'; +import testRunsStyles from '@/styles/test-runs/TestRunsPage.module.css'; +import footerStyles from '@/styles/Footer.module.css'; + +export function getHeightOfHeaderAndFooter(): number { + const classNames = [ + 'cds--header__global', + breadcrumbStyles.crumbContainer, + testRunsStyles.toolbar, + footerStyles.footer, + ]; + + let totalHeight = 0; + + classNames.forEach((className) => { + const elements = document.getElementsByClassName(className); + + // Iterate through all elements with this class name + for (let i = 0; i < elements.length; i++) { + const element = elements[i] as HTMLElement; + // Get the offsetHeight which includes padding and border + totalHeight += element.offsetHeight; + console.log('Hello ' + className + ': ' + element.offsetHeight); + } + }); + console.log('Hello ' + totalHeight); + return totalHeight; +}