Skip to content

Fixes #39253 - Implement "Not Found" error handling for invalid Job I…#1030

Merged
MariaAga merged 2 commits into
theforeman:masterfrom
pondrejk:invocations-empty-page
Jul 13, 2026
Merged

Fixes #39253 - Implement "Not Found" error handling for invalid Job I…#1030
MariaAga merged 2 commits into
theforeman:masterfrom
pondrejk:invocations-empty-page

Conversation

@pondrejk

Copy link
Copy Markdown
Contributor

…nvocation IDs

SAT-44544

image

@adamruzicka adamruzicka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some nitpicks, seems to work well.

<EmptyStateBody>
{sprintf(
__(
'There is no job invocation with id %s or there are access permissions needed. Please contact your administrator if this issue continues.'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From the top of my head, I could envision four error cases that can happen:

  1. The user doesn't have permission to view job invocations at all (backend should give 403 unauthorized)
  2. The user doesn't have permission to view this particular job invocation (backend should give 404)
  3. The user has permission to view, but the job invocation doesn't exist (backend should give 404)
  4. The backend failing to process the request (think 500 ISE)

Should we distinguish 1 vs 2+3 vs 4?

With that being said, the old page showed "Not found" for all cases, except 4 so feel free treat this as optional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Would it make sense to include the backend message to the page? It could be more informative without us needing to introduce some parsing logic, wdyt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure if the backend says anything reasonable, but worth a try.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In some cases the backend adds a nice message like for case 3 »Resource job_invocation not found by id '88'«

<EmptyStateBody>
{sprintf(
__(
'There is no job invocation with id %s or there are access permissions needed. Please contact your administrator if this issue continues.'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we know what the permissions required are, should we list them as we do on the unauthorized page?


const jobInvocationsIndexPath = '/job_invocations';

const JobInvocationEmptyState = ({ jobInvocationId }) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIR we have some patterns in core:

Should we re-use them instead of implementing a brand new specific page for it? I mean if they are not feasible, that's okay, I'm just asking if it was considered? I might lag behind our UI changes, so not sure if those aren't marked for future removal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did consider them, but found them a bit too restrictive. Otoh, if its worth it I can also extend them based on what we agree upon here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about them was too restrictive? might be worth to extend them as you said

@MariaAga MariaAga self-assigned this Apr 28, 2026
@pondrejk pondrejk force-pushed the invocations-empty-page branch 2 times, most recently from 236a8c6 to afbb4da Compare May 12, 2026 13:33
@pondrejk

Copy link
Copy Markdown
Contributor Author

@ofedoren @MariaAga @adamruzicka since the last time:

  • I changed thing so that the EmptyStatePattern from core is used
  • I added a note about needed perms
  • I display also the message from server
Screenshot 2026-05-12 at 15 41 38

@ofedoren ofedoren left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's more a high level review, I'd rather leave the React stuff for someone else :)

  1. One of the reasons why I wanted to re-use or enhance and re-use something from Foreman is because we're still trying to unify the UI across the pages at some degree.

Looking at the recent change to models page in Foreman and here I see a bunch of common things, essentially a lot of repetitions, which makes it harder to unify in the future if we'd need some changes. Anyway, at least now these two pages have at least different strings:

Image Image

It just feels odd :/

  1. We'd rather add tests, especially since we can use AI for that, just make sure it actually tests something.

  2. There is yet another custom error formatter. And AFAIU there is an idea of creating a general formatter that can be re-used then in other places. Should we wait for that or at least mark somehow to revisit this one and replace after the general/common one is available.

  3. Some inline comments.


const jobInvocationsIndexPath = '/job_invocations';

const JobInvocationEmptyState = ({ jobInvocationId, errorMessage = null }) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We already use defaultProps for that

Suggested change
const JobInvocationEmptyState = ({ jobInvocationId, errorMessage = null }) => {
const JobInvocationEmptyState = ({ jobInvocationId, errorMessage }) => {

@pondrejk

Copy link
Copy Markdown
Contributor Author

Looking at the recent change to models page in Foreman and here I see a bunch of common things, essentially a lot of repetitions, which makes it harder to unify in the future if we'd need some changes.

Well I thought the point of these patterns was to declare the overall shape of things for the implementations to fill in their own content that could be site specific. To me they look fairly unified, I can extend the foreman pattern, but then we'd need to agree on which parts should be unified and which should stay modifiable

@pondrejk

Copy link
Copy Markdown
Contributor Author

this is now dependent on theforeman/foreman#10986

@adamruzicka adamruzicka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This will also require a required foreman version bump in engine.rb, ideally as a separate commit.

@pondrejk

pondrejk commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

This will also require a required foreman version bump in engine.rb, ideally as a separate commit.

Hi, opened #1039

Comment on lines +25 to +31
onClick: () => visit(foremanUrl(jobInvocationsIndexPath)),
ouiaId: 'job-invocation-empty-state-go-to-job-invocations-button',
}}
secondaryActions={[
{
label: __('Create a new job invocation'),
onClick: () => visit(foremanUrl('/job_invocations/new')),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems that ResourceLoadFailedEmptyState accepts urls, and uses react-router for the actions (which is faster than visit), could the onClicks be replaced with a url?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I tried it out but didn't work for me, maybe because /job_invocations is still a rails index page?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what way did it not work? I tried it out locally and using

      primaryAction={{
        label: __('Go to job invocations'),
        url: foremanUrl(jobInvocationsIndexPath),
        ouiaId: 'job-invocation-empty-state-go-to-job-invocations-button',
      }}
      secondaryActions={[
        {
          label: __('Create a new job invocation'),
          url: foremanUrl('/job_invocations/new'),
          ouiaId: 'job-invocation-empty-state-create-new-job-invocation-button',
        },
      ]}

Works, and navigates

@pondrejk pondrejk Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

right, works for me to :) honestly, I don't know what was the issue last time I tried, sorry for the confusion

Comment thread webpack/JobInvocationDetail/index.js Outdated
Comment on lines +61 to +67
const { status: permissionsApiStatus } = useAPI(
'get',
currentPermissionsUrl,
{
key: CURRENT_PERMISSIONS,
}
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I do think its good to check if the permissions api failed, but I think this logic should be moved to core (in a new PR, unrelated to this one)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how to go about this, since permissionsApiStatus is one of the things we use to find what errorMessage to send to the ResourceLoadFailedEmptyState. Can you suggest where should I put that logic in core and how I'd trigger it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry my bad, I rechecked, and the data from currentPermissionsUrl is already in useForemanPermissions in the empty state, so no need to call it at all.

Foreman ui meta data (which goes to useForemanPermissions)
permissions: (User.current.admin? ? Permission.all : User.current.permissions).pluck(:name),
https://github.com/theforeman/foreman/blob/07ce9438d216cf7d1cef3763de66b83f22669c16/app/helpers/application_helper.rb#L422

api call to currentPermissionsUrl
@current_permissions = @user.admin? ? Permission.all : @user.permissions
https://github.com/theforeman/foreman/blob/07ce9438d216cf7d1cef3763de66b83f22669c16/app/controllers/api/v2/permissions_controller.rb#L32

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

makes sense, so I removed those currentPermissionsUrl calls

@MariaAga MariaAga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the empty state unloads the job invocation page when the api is done, with an error, it causes some components (JobInvocationToolbarButtons) to unload to fast and cause console warnings. I think this could be fixed by doing something like toolbarButtons={ jobInvocationApiStatus === API_STATUS.SUCCESS && <JobInvocationToolbarButtons jobId={id} data={items} />}

@pondrejk

Copy link
Copy Markdown
Contributor Author

Since the empty state unloads the job invocation page when the api is done, with an error, it causes some components (JobInvocationToolbarButtons) to unload to fast and cause console warnings. I think this could be fixed by doing something like toolbarButtons={ jobInvocationApiStatus === API_STATUS.SUCCESS && <JobInvocationToolbarButtons jobId={id} data={items} />}

thanks for noticing, I added your suggestion, but I had to also add some isMounted conditions to JobInvocationToolbarButtons for the console warning to stop

@pondrejk pondrejk force-pushed the invocations-empty-page branch from 8f0283f to 5ce73bc Compare June 25, 2026 11:30
@pondrejk

Copy link
Copy Markdown
Contributor Author

@MariaAga Hi, I think I've addressed all the comments, can you plrease take a look again?

@MariaAga MariaAga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I ran an AI review, and it caught that with the api permisisons removal, the toolbar and the actions, dont get to see the correct permissions, so it will be good to switch out the permissions checks in webpack/JobInvocationDetail/CheckboxesActions.js and webpack/JobInvocationDetail/JobInvocationToolbarButtons.js with the usePermissions hook. (this causes the buttons next to the search bar to always be disabled)

Also missing a test to make sure the empty state loads.

Comment thread webpack/JobInvocationDetail/index.js Outdated
breadcrumbOptions={breadcrumbOptions}
toolbarButtons={<JobInvocationToolbarButtons jobId={id} data={items} />}
toolbarButtons={
jobInvocationApiStatus === API_STATUS.SUCCESS && (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The STATUS constant from foremanReact/constants has keys PENDING, RESOLVED, and ERROR — there is no SUCCESS. This comparison always evaluates to false, so
JobInvocationToolbarButtons is never rendered regardless of API state. Should be API_STATUS.RESOLVED.

Comment thread webpack/JobInvocationDetail/index.js Outdated
return null;
}, [jobInvocationApiStatus, jobInvocationErrorMessage]);

const httpStatus = jobInvocationHttpStatus;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Used once, no transformation. Pass jobInvocationHttpStatus directly to the prop.

Comment on lines +3 to +5
export const visit = jest.fn(url => {
global.window.location.href = url;
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not used

import { foremanUrl } from 'foremanReact/common/helpers';
import ResourceLoadFailedEmptyState from 'foremanReact/components/common/EmptyState/ResourceLoadFailedEmptyState';

const jobInvocationsIndexPath = '/job_invocations';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should live in JobInvocationConstants.js alongside jobInvocationDetailsUrl.

@pondrejk

pondrejk commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

I ran an AI review, and it caught that with the api permisisons removal, the toolbar and the actions, dont get to see the correct permissions, so it will be good to switch out the permissions checks in webpack/JobInvocationDetail/CheckboxesActions.js and webpack/JobInvocationDetail/JobInvocationToolbarButtons.js with the usePermissions hook. (this causes the buttons next to the search bar to always be disabled)

Also missing a test to make sure the empty state loads.

Hi @MariaAga, I did the requested updates, please take a look when you have time

@MariaAga MariaAga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! PR logic works as expected, but needs some code changes

We prefer to test what the user sees, rather than relying on test ids, so screen.getByTestId( 'job-invocation-empty-state-go-to-job-invocations-button' ) should be changed to things like screen.getByRole('button', { name: 'Create a new job invocation' })

You can read more about testing guidelines here: https://github.com/theforeman/foreman/blob/develop/developer_docs/ui-testing-guidelines.asciidoc


jest.mock('foremanReact/common/hooks/Permissions/permissionHooks', () => ({
usePermissions: jest.fn(() => true),
}));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the mock is the same as the mock file, no need to mock it again

Comment on lines +8 to +10
jest.mock('foremanReact/common/hooks/Permissions/permissionHooks', () => ({
usePermissions: jest.fn(() => true),
}));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the mock is the same as the mock file, no need to mock it again

Comment on lines +12 to +39
jest.mock(
'foremanReact/components/common/EmptyState/ResourceLoadFailedEmptyState',
() => ({
__esModule: true,
default: ({
resourceLabel,
resourceId,
errorMessage,
primaryAction,
secondaryActions,
ouiaIdPrefix,
}) => (
<div data-testid={ouiaIdPrefix}>
<h1>{`Unable to load ${resourceLabel}`}</h1>
<p>{`The ${resourceLabel} with id ${resourceId} could not be found.`}</p>
{errorMessage && <p>{`Server returned: ${errorMessage}`}</p>}
<button type="button" data-testid={primaryAction.ouiaId}>
{primaryAction.label}
</button>
{secondaryActions.map(action => (
<button key={action.ouiaId} type="button" data-testid={action.ouiaId}>
{action.label}
</button>
))}
</div>
),
})
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Prefer just relying on the original component, rather than mocking it.

Comment on lines +65 to +87
jest.mock(
'foremanReact/components/common/EmptyState/ResourceLoadFailedEmptyState',
() => ({
__esModule: true,
default: ({
resourceLabel,
resourceId,
errorMessage,
primaryAction,
ouiaIdPrefix,
}) => (
<div data-testid={ouiaIdPrefix}>
<h1>{`Unable to load ${resourceLabel}`}</h1>
<p>{`The ${resourceLabel} with id ${resourceId} could not be found.`}</p>
{errorMessage && <p>{`Server returned: ${errorMessage}`}</p>}
<button type="button" data-testid={primaryAction.ouiaId}>
{primaryAction.label}
</button>
</div>
),
})
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Prefer just relying on the original component, rather than mocking it.

Comment on lines 55 to 59
@@ -50,6 +58,33 @@ jest.mock('foremanReact/common/hooks/API/APIHooks', () => ({
})),
}));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

useAPI is no longer used

templateInvocationPageUrl,
} from './JobInvocationConstants';
import {
selectHasPermission,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since this is no longer used, it can be removed from JobInvocationSelectors

Comment on lines -25 to -30
CURRENT_PERMISSIONS,
DATE_OPTIONS,
JOB_INVOCATION_KEY,
STATUS,
STATUS_UPPERCASE,
currentPermissionsUrl,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since this (currentPermissionsUrl,CURRENT_PERMISSIONS) is no longer used, it can be removed from JobInvocationConstants

Comment on lines +24 to +30
url: foremanUrl(jobInvocationsIndexPath),
ouiaId: 'job-invocation-empty-state-go-to-job-invocations-button',
}}
secondaryActions={[
{
label: __('Create a new job invocation'),
url: foremanUrl('/job_invocations/new'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

both urls should be consistent, and the foremanUrl can be moved to the consts

Comment on lines 32 to 33
export const selectCurrentPermisions = state =>
selectAPIResponse(state, CURRENT_PERMISSIONS);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can also be removed since its not used, and CURRENT_PERMISSIONS is undefined.

@pondrejk pondrejk force-pushed the invocations-empty-page branch from 95d9efa to 4f79c5f Compare July 9, 2026 10:53
@MariaAga

MariaAga commented Jul 9, 2026

Copy link
Copy Markdown
Member

I started testing that the page still works, but now when a job is running the buttons (create report, rerun all) are flashing, and it didnt seem to happen without the pr

Screencast.From.2026-07-09.12-52-16.mp4

@pondrejk pondrejk force-pushed the invocations-empty-page branch from 0a770c2 to ec5e58e Compare July 10, 2026 12:48
@pondrejk

Copy link
Copy Markdown
Contributor Author

I started testing that the page still works, but now when a job is running the buttons (create report, rerun all) are flashing, and it didnt seem to happen without the pr

great chatch! I did some update to fix that with ai assistance, I'm posting it as a separate commit for reviewability

@MariaAga MariaAga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! code lgtm, and I tested it

@MariaAga MariaAga merged commit 61739ea into theforeman:master Jul 13, 2026
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.

4 participants