Skip to content

Fixes #38806 - Stabilize job detail table and fix unresponsive actions#1009

Merged
adamruzicka merged 1 commit intotheforeman:masterfrom
kmalyjur:rerun-failed
Nov 12, 2025
Merged

Fixes #38806 - Stabilize job detail table and fix unresponsive actions#1009
adamruzicka merged 1 commit intotheforeman:masterfrom
kmalyjur:rerun-failed

Conversation

@kmalyjur
Copy link
Contributor

@kmalyjur kmalyjur commented Oct 6, 2025

Initial issue:
The "Rerun" button in the Job Details table would inconsistently become unresponsive.

My thoughts:
I couldn't reproduce this bug, however, I still decided to make this change. When a row was collapsed, the react state was destroyed, which could break event handlers on other rows during a re-render.

Solution:
Lifting the component state to the JobInvocationHostTable.

@kmalyjur kmalyjur changed the title Stabilize job detail table and fix unresponsive actions Fixes #38806 - Stabilize job detail table and fix unresponsive actions Oct 6, 2025
@kmalyjur kmalyjur force-pushed the rerun-failed branch 2 times, most recently from 34933fa to 962f783 Compare October 8, 2025 09:03
@kmalyjur kmalyjur marked this pull request as ready for review October 8, 2025 09:20
@kmalyjur kmalyjur requested a review from MariaAga October 8, 2025 09:20
Comment on lines 65 to 76
act(() => {
fireEvent.click(screen.getByText('STDOUT'));
fireEvent.click(screen.getByText('DEBUG'));
fireEvent.click(screen.getByText('STDERR'));
});
expect(
screen.queryAllByText('No output for the selected filters')
).toHaveLength(1);
expect(screen.queryAllByText('Exit status: 1')).toHaveLength(0);
expect(
screen.queryAllByText('StandardError: Job execution failed')
).toHaveLength(0);
Copy link
Member

Choose a reason for hiding this comment

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

These tests need to be moved, not removed

Comment on lines +69 to +75
const getInvocationState = hostId =>
hostInvocationStates[hostId] || {
showOutputType: { stderr: true, stdout: true, debug: true },
showTemplatePreview: false,
showCommand: false,
};

Copy link
Member

Choose a reason for hiding this comment

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

It also needs to be used in webpack/JobInvocationDetail/TemplateInvocationPage.js

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes issue #38806 by stabilizing the job detail table and resolving unresponsive action buttons. The solution lifts component state from individual TemplateInvocation components to the parent JobInvocationHostTable to prevent state destruction when rows are collapsed/expanded.

  • Moved template invocation state (output toggles, preview settings) from child to parent component
  • Refactored tests to remove interactive event testing and use prop-based state management
  • Added CSS styling to properly hide collapsed rows

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
TemplateInvocation.js Removed internal state management, now receives state as props
JobInvocationHostTable.js Added centralized state management for all host invocation states
OutputToggleGroup.js Updated to use direct state values instead of functional updates
TemplateInvocation.test.js Refactored tests to use prop-based state instead of simulated user interactions
JobInvocationDetail.scss Added CSS rule to hide collapsed table rows

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines 30 to 47
const handleSTDERRClick = _isSelected => {
setShowOutputType(prevShowOutputType => ({
...prevShowOutputType,
setShowOutputType({
...showOutputType,
stderr: _isSelected,
}));
});
};

const handleSTDOUTClick = _isSelected => {
setShowOutputType(prevShowOutputType => ({
...prevShowOutputType,
setShowOutputType({
...showOutputType,
stdout: _isSelected,
}));
});
};
const handleDEBUGClick = _isSelected => {
setShowOutputType(prevShowOutputType => ({
...prevShowOutputType,
setShowOutputType({
...showOutputType,
debug: _isSelected,
}));
});
};
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

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

The state update functions create new objects on every call, which could cause unnecessary re-renders. Consider using functional updates or memoizing these handlers to avoid creating new objects when the current state hasn't changed.

Copilot uses AI. Check for mistakes.
jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
isInTableView: PropTypes.bool,
isExpanded: PropTypes.bool,
showOutputType: PropTypes.object.isRequired,
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

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

Using PropTypes.object is too generic. Consider using PropTypes.shape() to define the specific structure expected: PropTypes.shape({ stderr: PropTypes.bool, stdout: PropTypes.bool, debug: PropTypes.bool }).isRequired

Suggested change
showOutputType: PropTypes.object.isRequired,
showOutputType: PropTypes.shape({
stderr: PropTypes.bool,
stdout: PropTypes.bool,
debug: PropTypes.bool,
}).isRequired,

Copilot uses AI. Check for mistakes.
Comment on lines 144 to 146
.pf-v5-c-table__tbody > tr.row-hidden {
display: none;
}
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be more specific

Comment on lines 123 to +125

if (!isExpanded) {
return null;
}

if ((status === STATUS.PENDING && isEmpty(response)) || !response) {
return <Skeleton />;
if (!response || (status === STATUS.PENDING && isEmpty(response))) {
return <Skeleton data-testid="template-invocation-skeleton" />;
Copy link
Member

Choose a reason for hiding this comment

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

Could you please explain this change?

Copy link
Contributor Author

@kmalyjur kmalyjur Oct 20, 2025

Choose a reason for hiding this comment

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

if (!isExpanded) { return null; }

  • Removed it so it hides the row instead of destroying it -> saves its state

data-testid

  • fixes failing test

Do you think it should be done differently?

Copy link
Member

Choose a reason for hiding this comment

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

That makes sense, thanks!

Copy link
Contributor

@adamruzicka adamruzicka left a comment

Choose a reason for hiding this comment

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

The lint issues seem to be a hard requirement

/>
<RowSelectTd
rowData={result}
{...{ selectOne, isSelected }}
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't the splat unnecessary?

Suggested change
{...{ selectOne, isSelected }}
{selectOne, isSelected}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Screenshot 2025-10-23 at 12 05 01

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, so the first pair of curly braces is to go from jsx into the "native js" context and then splat the hash inside it? Wouldn't this then be the same thing as

Suggested change
{...{ selectOne, isSelected }}
selectOne
isSelected

I don't insist on it, just trying to figure out how these things work

Copy link
Member

Choose a reason for hiding this comment

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

another way would be just

Suggested change
{...{ selectOne, isSelected }}
selectOne={selecteOne}
isSelected={isSelected}

@@ -64,6 +64,13 @@ const JobInvocationHostTable = ({
const [expandedHost, setExpandedHost] = useState([]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Although not introduced here, a Set would be a more appropriate data structure, considering how it is being used

Copy link
Contributor

Choose a reason for hiding this comment

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

This will also need changes in JobInvocationHostTable.js:352

Uncaught TypeError: expandedHost.includes is not a function
    at JobInvocationHostTable.js:352:46
    at Array.every (<anonymous>)
    at JobInvocationHostTable (JobInvocationHostTable.js:352:17)

@kmalyjur
Copy link
Contributor Author

@MariaAga @adamruzicka thank you for your reviews! Could you please check if I incorporated everything?

Copy link
Contributor

@adamruzicka adamruzicka left a comment

Choose a reason for hiding this comment

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

To be explicit about #1009 (comment)

Copy link
Contributor

@adamruzicka adamruzicka left a comment

Choose a reason for hiding this comment

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

Looks good, I haven't seen any oddities with this

Comment on lines 14 to 27
jest.mock('@patternfly/react-core', () => {
const originalModule = jest.requireActual('@patternfly/react-core');
return {
...originalModule,
Tooltip: ({ children }) => <>{children}</>,
Popper: ({ children }) => <>{children}</>,
ClipboardCopyButton: jest.fn(props => (
<button data-testid="mock-clipboard-copy-button">
{props.children || 'Mock Copy Button'}
</button>
)),
};
});

Copy link
Member

Choose a reason for hiding this comment

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

Could you please explain this mock is needed?

Copy link
Contributor Author

@kmalyjur kmalyjur Nov 11, 2025

Choose a reason for hiding this comment

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

@MariaAga Sure, the tests were crashing while trying to render this button, so I made this mock. If you think it's bad, I can try to find different solution.

Copy link
Member

Choose a reason for hiding this comment

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

I would rather test components fully, so if we need to mock something because it crashes, to me it looks like it might also crash when users are using it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Member

@MariaAga MariaAga left a comment

Choose a reason for hiding this comment

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

Tested on my dev setup and it seem to work, I just have a question about the mock in the test

Copy link
Member

@MariaAga MariaAga left a comment

Choose a reason for hiding this comment

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

lgtm, @adamruzicka can this be merged now, or do we need to wait because of branching?

@adamruzicka
Copy link
Contributor

We can get this in straight away, thank you both

@adamruzicka adamruzicka merged commit 41b8dee into theforeman:master Nov 12, 2025
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants