Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion galasa-ui/src/components/test-runs/TestRunsDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -45,6 +46,7 @@ export default function TestRunsDetails({
const [notification, setNotification] = useState<NotificationType | null>(null);
const [isEditingName, setIsEditingName] = useState<boolean>(false);
const [editedName, setEditedName] = useState<string>('');
const [maxHeight, setMaxHeight] = useState<string>('68vh');

const inputRef = useRef<HTMLInputElement>(null);

Expand All @@ -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);
};
}, []);
Comment on lines +63 to +79
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some code duplication now with the content of this useEffect and the useEffect in the CollapsibleSideBar component.

Is there a way to reduce this duplication?


const handleShare = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
Expand Down Expand Up @@ -247,7 +267,7 @@ export default function TestRunsDetails({
<div className={styles.testRunsContentWrapper}>
<CollapsibleSideBar handleEditQueryName={handleStartEditingName} />

<div className={styles.mainContent}>
<div className={styles.mainContent} style={{ maxHeight }}>
<TestRunsSearch />
<div className={styles.queryNameContainer}>
<QueryName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useEffect, useMemo, useState } from 'react';
import { HeaderMenuButton, Search, Button, InlineNotification } from '@carbon/react';
import { Add } from '@carbon/icons-react';
import styles from '@/styles/test-runs/saved-queries/CollapsibleSideBar.module.css';
import testRunsPageStyles from '@/styles/test-runs/TestRunsPage.module.css';
import {
arrayMove,
SortableContext,
Expand Down Expand Up @@ -40,6 +39,7 @@ import {
} from '@/utils/constants/common';
import { encodeStateToUrlParam } from '@/utils/encoding/urlEncoder';
import { generateUniqueQueryName } from '@/utils/functions/savedQueries';
import { getHeightOfHeaderAndFooter } from '@/utils/functions/getHeightOfHeaderAndFooter';

interface CollapsibleSideBarProps {
handleEditQueryName: (queryName: string) => void;
Expand All @@ -54,15 +54,11 @@ export default function CollapsibleSideBar({ handleEditQueryName }: CollapsibleS
const [notification, setNotification] = useState<NotificationType | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [maxHeight, setMaxHeight] = useState<string>('68vh');

// State to hold the data of the item currently being dragged for the DragOverlay
const [activeQuery, setActiveQuery] = useState<SavedQueryType | null>(null);

const [sideNavExpandedHeight, setSideNavExpandedHeight] = useState(0);
const [mainContentElement, setMainContentElement] = useState<Element | null>(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),
Expand Down Expand Up @@ -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 (
<div className={styles.container} aria-label={translations('savedQueriesHeaderLabel')}>
Expand All @@ -215,10 +184,9 @@ export default function CollapsibleSideBar({ handleEditQueryName }: CollapsibleS
onClick={() => setIsExpanded(!isExpanded)}
/>

<div className={styles.sidebarWrapper}>
<div className={styles.sidebarWrapper} style={{ maxHeight }}>
<div
className={isExpanded ? styles.sideNavExpanded : styles.sideNavCollapsed}
style={{ height: sideNavExpandedHeight }}
aria-label={translations('savedQueriesSidebarLabel')}
>
<div className={styles.innerContentWrapper}>
Expand Down
1 change: 1 addition & 0 deletions galasa-ui/src/styles/common/BreadCrumb.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
*/

.crumbContainer {
height: 84px;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manually set this height otherwise it seems to default to 64px for an odd reason.

padding: 2rem 1rem;
}
5 changes: 1 addition & 4 deletions galasa-ui/src/styles/test-runs/TestRunsPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
flex: 1;
min-width: 0;
padding: 2.5rem 4rem;
overflow: auto;
}

@media (max-width: 768px) {
Expand All @@ -34,10 +35,6 @@
}
}

.tabsContainer {
height: 100%;
}

.titleText {
font-size: 1rem;
margin: 0.5rem 0.5rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<CollapsibleSideBar handleEditQueryName={mockHandleEditQueryName} />);
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(<CollapsibleSideBar handleEditQueryName={mockHandleEditQueryName} />);

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 = '';
});
});
});
});
164 changes: 164 additions & 0 deletions galasa-ui/src/tests/utils/functions/getHeightOfHeaderAndFooter.test.ts
Original file line number Diff line number Diff line change
@@ -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
});
});
Loading