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, + }); + }); + }); });