diff --git a/assets/js/common/SuseManagerConfig/SuseManagerSettingsContainer.jsx b/assets/js/common/SuseManagerConfig/SuseManagerSettingsContainer.jsx
new file mode 100644
index 0000000000..1ce20a22f4
--- /dev/null
+++ b/assets/js/common/SuseManagerConfig/SuseManagerSettingsContainer.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+
+import ConnectionErrorAntenna from '@static/connection-error-antenna.svg';
+
+import LoadingBox from '@common/LoadingBox';
+import NotificationBox from '@common/NotificationBox';
+
+function SuseManagerSettingsContainer({ error, loading, children, onRetry }) {
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ }
+ title="Connection Error"
+ text="Unable to load SUSE Manager configuration. Please try reloading this section."
+ buttonText="Reload"
+ buttonOnClick={onRetry}
+ />
+ );
+ }
+
+ return children;
+}
+
+export default SuseManagerSettingsContainer;
diff --git a/assets/js/common/SuseManagerConfig/SuseManagerSettingsContainer.test.jsx b/assets/js/common/SuseManagerConfig/SuseManagerSettingsContainer.test.jsx
new file mode 100644
index 0000000000..7da5ce35c0
--- /dev/null
+++ b/assets/js/common/SuseManagerConfig/SuseManagerSettingsContainer.test.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { faker } from '@faker-js/faker';
+
+import { screen, render } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+import SuseManagerSettingsContainer from './SuseManagerSettingsContainer';
+
+describe('ChecksCatalog CatalogContainer component', () => {
+ it('should render the loading box', () => {
+ const text = faker.lorem.paragraph();
+
+ render(
+
+ {text}
+
+ );
+
+ expect(screen.getByText(text)).toBeVisible();
+ });
+
+ it('should render the notification box in loading state', () => {
+ render();
+
+ expect(screen.getByText('Loading Settings...')).toBeVisible();
+ });
+
+ it('should render the notification box with connection error', () => {
+ render();
+
+ expect(screen.getByText('Connection Error')).toBeVisible();
+ expect(
+ screen.getByText(
+ 'Unable to load SUSE Manager configuration. Please try reloading this section.'
+ )
+ ).toBeVisible();
+ expect(screen.getByRole('button')).toHaveTextContent('Reload');
+ });
+});
diff --git a/assets/js/common/SuseManagerConfig/index.js b/assets/js/common/SuseManagerConfig/index.js
index 0d3f135e92..d4cf436732 100644
--- a/assets/js/common/SuseManagerConfig/index.js
+++ b/assets/js/common/SuseManagerConfig/index.js
@@ -1,3 +1,6 @@
import SuseManagerConfig from './SuseManagerConfig';
+import SuseManagerSettingsContainer from './SuseManagerSettingsContainer';
export default SuseManagerConfig;
+
+export { SuseManagerSettingsContainer };
diff --git a/assets/js/pages/SettingsPage/SettingsPage.jsx b/assets/js/pages/SettingsPage/SettingsPage.jsx
index 0d662caf60..dd8585e711 100644
--- a/assets/js/pages/SettingsPage/SettingsPage.jsx
+++ b/assets/js/pages/SettingsPage/SettingsPage.jsx
@@ -9,9 +9,11 @@ import { logError } from '@lib/log';
import { get, patch } from '@lib/network';
import { getFromConfig } from '@lib/config';
-import LoadingBox from '@common/LoadingBox';
import PageHeader from '@common/PageHeader';
import Button from '@common/Button';
+import SuseManagerConfig, {
+ SuseManagerSettingsContainer,
+} from '@common/SuseManagerConfig';
import SuseManagerSettingsModal from '@common/SuseManagerSettingsDialog';
import ApiKeySettingsModal from '@common/ApiKeySettingsModal';
@@ -32,8 +34,6 @@ import {
import { dismissNotification } from '@state/notifications';
import { API_KEY_EXPIRATION_NOTIFICATION_ID } from '@state/sagas/settings';
-import SuseManagerConfig from '@common/SuseManagerConfig';
-
function SettingsPage() {
const dispatch = useDispatch();
@@ -84,6 +84,7 @@ function SettingsPage() {
settings,
loading: softwareUpdatesSettingsLoading,
editing: editingSoftwareUpdatesSettings,
+ networkError: softwareUpdatesSettingsNetworkError,
testingConnection: testingSoftwareUpdatesConnection,
} = useSelector(getSoftwareUpdatesSettings);
@@ -253,12 +254,11 @@ function SettingsPage() {
/>
{getFromConfig('suseManagerEnabled') && (
- {softwareUpdatesSettingsLoading ? (
-
- ) : (
+ dispatch(fetchSoftwareUpdatesSettings())}
+ >
setClearingSoftwareUpdatesSettings(false)}
/>
- )}
+
{
]);
});
- it('should empty software updates settings on failed fetching', async () => {
+ it('should empty software updates settings when no configured settings were found', async () => {
const axiosMock = new MockAdapter(networkClient);
- [404, 500].forEach(async (errorStatus) => {
- axiosMock.onGet('/settings/suma_credentials').reply(errorStatus);
+ const mockAuthClient = new MockAdapter(authClient);
+ storeRefreshToken('refresh-token');
+
+ mockAuthClient.onPost('/api/session/refresh').reply(200, {
+ access_token: 'new_token',
+ });
+
+ axiosMock
+ .onGet('/settings/suma_credentials')
+ .replyOnce(401, {
+ error: 'unauthorized',
+ })
+ .onGet('/settings/suma_credentials')
+ .reply(401);
+
+ const dispatched = await recordSaga(fetchSoftwareUpdatesSettings);
+
+ expect(dispatched).toEqual([
+ startLoadingSoftwareUpdatesSettings(),
+ setEmptySoftwareUpdatesSettings(),
+ ]);
+ });
+
+ it.each([403, 404, 500, 502, 504])(
+ 'should empty software updates settings and put a network error flag on failed fetching',
+ async (status) => {
+ const axiosMock = new MockAdapter(networkClient);
+ axiosMock.onGet('/settings/suma_credentials').reply(status);
const dispatched = await recordSaga(fetchSoftwareUpdatesSettings);
expect(dispatched).toEqual([
startLoadingSoftwareUpdatesSettings(),
setEmptySoftwareUpdatesSettings(),
+ setNetworkError(true),
]);
- });
- });
+ }
+ );
});
describe('Saving Software Updates settings', () => {
diff --git a/assets/js/state/selectors/softwareUpdatesSettings.js b/assets/js/state/selectors/softwareUpdatesSettings.js
index 6b0eb898f8..1fb3a0b5fc 100644
--- a/assets/js/state/selectors/softwareUpdatesSettings.js
+++ b/assets/js/state/selectors/softwareUpdatesSettings.js
@@ -2,11 +2,19 @@ import { createSelector } from '@reduxjs/toolkit';
export const getSoftwareUpdatesSettings = createSelector(
[({ softwareUpdatesSettings }) => softwareUpdatesSettings],
- ({ settings, errors, loading, editing, testingConnection }) => ({
+ ({
settings,
errors,
loading,
editing,
+ networkError,
+ testingConnection,
+ }) => ({
+ settings,
+ errors,
+ loading,
+ editing,
+ networkError,
testingConnection,
})
);
diff --git a/assets/js/state/softwareUpdatesSettings.js b/assets/js/state/softwareUpdatesSettings.js
index dbdcde6979..896c36dba6 100644
--- a/assets/js/state/softwareUpdatesSettings.js
+++ b/assets/js/state/softwareUpdatesSettings.js
@@ -12,7 +12,7 @@ const emptySettings = {
const initialState = {
loading: false,
settings: emptySettings,
- networkError: null,
+ networkError: false,
editing: false,
testingConnection: false,
errors: [],
@@ -27,17 +27,17 @@ export const softwareUpdatesSettingsSlice = createSlice({
},
setSoftwareUpdatesSettings: (state, { payload: settings }) => {
state.loading = false;
- state.networkError = null;
+ state.networkError = false;
state.settings = settings;
},
setEmptySoftwareUpdatesSettings: (state) => {
state.loading = false;
- state.networkError = null;
+ state.networkError = false;
state.settings = emptySettings;
},
setSoftwareUpdatesSettingsErrors: (state, { payload: errors }) => {
state.loading = false;
- state.networkError = null;
+ state.networkError = false;
state.errors = errors;
},
setEditingSoftwareUpdatesSettings: (state, { payload }) => {
@@ -46,6 +46,10 @@ export const softwareUpdatesSettingsSlice = createSlice({
setTestingSoftwareUpdatesConnection: (state, { payload }) => {
state.testingConnection = payload;
},
+ setNetworkError: (state, { payload }) => {
+ state.loading = false;
+ state.networkError = payload;
+ },
},
});
@@ -83,6 +87,7 @@ export const {
setSoftwareUpdatesSettingsErrors,
setEditingSoftwareUpdatesSettings,
setTestingSoftwareUpdatesConnection,
+ setNetworkError,
} = softwareUpdatesSettingsSlice.actions;
export default softwareUpdatesSettingsSlice.reducer;
diff --git a/assets/js/state/softwareUpdatesSettings.test.js b/assets/js/state/softwareUpdatesSettings.test.js
index 5d3bcaef72..2c1c7a25ac 100644
--- a/assets/js/state/softwareUpdatesSettings.test.js
+++ b/assets/js/state/softwareUpdatesSettings.test.js
@@ -5,6 +5,7 @@ import softwareUpdatesSettingsReducer, {
setSoftwareUpdatesSettingsErrors,
setEditingSoftwareUpdatesSettings,
setTestingSoftwareUpdatesConnection,
+ setNetworkError,
} from './softwareUpdatesSettings';
describe('SoftwareUpdateSettings reducer', () => {
@@ -32,7 +33,7 @@ describe('SoftwareUpdateSettings reducer', () => {
username: undefined,
ca_uploaded_at: undefined,
},
- networkError: null,
+ networkError: false,
errors: [],
};
@@ -49,7 +50,7 @@ describe('SoftwareUpdateSettings reducer', () => {
expect(actual).toEqual({
loading: false,
settings,
- networkError: null,
+ networkError: false,
errors: [],
});
});
@@ -62,7 +63,7 @@ describe('SoftwareUpdateSettings reducer', () => {
username: 'username',
ca_uploaded_at: '2021-01-01T00:00:00Z',
},
- networkError: null,
+ networkError: false,
errors: [],
};
@@ -77,7 +78,7 @@ describe('SoftwareUpdateSettings reducer', () => {
username: undefined,
ca_uploaded_at: undefined,
},
- networkError: null,
+ networkError: false,
errors: [],
});
});
@@ -90,7 +91,7 @@ describe('SoftwareUpdateSettings reducer', () => {
username: 'username',
ca_uploaded_at: '2021-01-01T00:00:00Z',
},
- networkError: null,
+ networkError: false,
errors: [],
};
@@ -118,7 +119,7 @@ describe('SoftwareUpdateSettings reducer', () => {
username: 'username',
ca_uploaded_at: '2021-01-01T00:00:00Z',
},
- networkError: null,
+ networkError: false,
errors,
});
});
@@ -131,7 +132,7 @@ describe('SoftwareUpdateSettings reducer', () => {
username: 'username',
ca_uploaded_at: '2021-01-01T00:00:00Z',
},
- networkError: null,
+ networkError: false,
errors: [],
editing: false,
};
@@ -154,7 +155,7 @@ describe('SoftwareUpdateSettings reducer', () => {
username: 'username',
ca_uploaded_at: '2021-01-01T00:00:00Z',
},
- networkError: null,
+ networkError: false,
errors: [],
editing: false,
testingConnection: false,
@@ -171,4 +172,31 @@ describe('SoftwareUpdateSettings reducer', () => {
});
});
});
+
+ it('should mark that the connection is being tested', () => {
+ const initialState = {
+ loading: true,
+ settings: {
+ url: 'https://valid.url',
+ username: 'username',
+ ca_uploaded_at: '2021-01-01T00:00:00Z',
+ },
+ networkError: false,
+ errors: [],
+ editing: false,
+ testingConnection: false,
+ };
+
+ [true, false].forEach((hasNetworkError) => {
+ const action = setNetworkError(hasNetworkError);
+
+ const actual = softwareUpdatesSettingsReducer(initialState, action);
+
+ expect(actual).toEqual({
+ ...initialState,
+ loading: false,
+ networkError: hasNetworkError,
+ });
+ });
+ });
});