diff --git a/ui/webui/src/actions/localization-actions.js b/ui/webui/src/actions/localization-actions.js index f02ae645d26..ab34bac58d4 100644 --- a/ui/webui/src/actions/localization-actions.js +++ b/ui/webui/src/actions/localization-actions.js @@ -23,49 +23,66 @@ import { getLocales, getLocaleData, } from "../apis/localization.js"; +import { setCriticalErrorAction } from "../actions/miscellaneous-actions.js"; export const getLanguagesAction = () => { return async (dispatch) => { - const languageIds = await getLanguages(); + try { + const languageIds = await getLanguages(); - return Promise.all([ - dispatch(getCommonLocalesAction()), - ...languageIds.map(language => dispatch(getLanguageDataAction({ language }))) - ]); + return Promise.all([ + dispatch(getCommonLocalesAction()), + ...languageIds.map(language => dispatch(getLanguageDataAction({ language }))) + ]); + } catch (error) { + dispatch(setCriticalErrorAction(error)); + } }; }; export const getLanguageDataAction = ({ language }) => { return async (dispatch) => { - const localeIds = await getLocales({ lang: language }); - const languageData = await getLanguageData({ lang: language }); - const locales = await Promise.all(localeIds.map(async locale => await getLocaleData({ locale }))); + try { + const localeIds = await getLocales({ lang: language }); + const languageData = await getLanguageData({ lang: language }); + const locales = await Promise.all(localeIds.map(async locale => await getLocaleData({ locale }))); - return dispatch({ - type: "GET_LANGUAGE_DATA", - payload: { languageData: { [language]: { languageData, locales } } } - }); + return dispatch({ + type: "GET_LANGUAGE_DATA", + payload: { languageData: { [language]: { languageData, locales } } } + }); + } catch (error) { + dispatch(setCriticalErrorAction(error)); + } }; }; export const getLanguageAction = () => { return async (dispatch) => { - const language = await getLanguage(); + try { + const language = await getLanguage(); - return dispatch({ - type: "GET_LANGUAGE", - payload: { language } - }); + return dispatch({ + type: "GET_LANGUAGE", + payload: { language } + }); + } catch (error) { + dispatch(setCriticalErrorAction(error)); + } }; }; export const getCommonLocalesAction = () => { return async (dispatch) => { - const commonLocales = await getCommonLocales(); + try { + const commonLocales = await getCommonLocales(); - return dispatch({ - type: "GET_COMMON_LOCALES", - payload: { commonLocales } - }); + return dispatch({ + type: "GET_COMMON_LOCALES", + payload: { commonLocales } + }); + } catch (error) { + dispatch(setCriticalErrorAction(error)); + } }; }; diff --git a/ui/webui/src/actions/miscellaneous-actions.js b/ui/webui/src/actions/miscellaneous-actions.js new file mode 100644 index 00000000000..c6f69a26614 --- /dev/null +++ b/ui/webui/src/actions/miscellaneous-actions.js @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with This program; If not, see . + */ + +export const setCriticalErrorAction = (criticalError) => ({ + type: "SET_CRITICAL_ERROR", + payload: { criticalError } +}); diff --git a/ui/webui/src/actions/network-actions.js b/ui/webui/src/actions/network-actions.js index f270db51047..bec46f8fd71 100644 --- a/ui/webui/src/actions/network-actions.js +++ b/ui/webui/src/actions/network-actions.js @@ -18,14 +18,19 @@ import { getConnected, } from "../apis/network.js"; +import { setCriticalErrorAction } from "../actions/miscellaneous-actions.js"; export const getConnectedAction = () => { return async (dispatch) => { - const connected = await getConnected(); + try { + const connected = await getConnected(); - return dispatch({ - type: "GET_NETWORK_CONNECTED", - payload: { connected } - }); + return dispatch({ + type: "GET_NETWORK_CONNECTED", + payload: { connected } + }); + } catch (error) { + setCriticalErrorAction(error); + } }; }; diff --git a/ui/webui/src/actions/storage-actions.js b/ui/webui/src/actions/storage-actions.js index f2225ff2cb2..be70c6573f8 100644 --- a/ui/webui/src/actions/storage-actions.js +++ b/ui/webui/src/actions/storage-actions.js @@ -28,73 +28,88 @@ import { getPartitioningMethod, getUsableDisks, } from "../apis/storage.js"; +import { + setCriticalErrorAction, +} from "../actions/miscellaneous-actions.js"; export const getDevicesAction = () => { return async (dispatch) => { - const devices = await getDevices(); - const devicesData = await Promise.all(devices[0].map(async (device) => { - let devData = await getDeviceData({ disk: device }); - devData = devData[0]; + try { + const devices = await getDevices(); + const devicesData = await Promise.all(devices[0].map(async (device) => { + let devData = await getDeviceData({ disk: device }); + devData = devData[0]; - const free = await getDiskFreeSpace({ diskNames: [device] }); - // extend it with variants to keep the format consistent - devData.free = cockpit.variant(String, free); + const free = await getDiskFreeSpace({ diskNames: [device] }); + // extend it with variants to keep the format consistent + devData.free = cockpit.variant(String, free); - const total = await getDiskTotalSpace({ diskNames: [device] }); - devData.total = cockpit.variant(String, total); + const total = await getDiskTotalSpace({ diskNames: [device] }); + devData.total = cockpit.variant(String, total); - const formatData = await getFormatData({ diskName: device }); - devData.formatData = formatData; + const formatData = await getFormatData({ diskName: device }); + devData.formatData = formatData; - const deviceData = { [device]: devData }; + const deviceData = { [device]: devData }; - return deviceData; - })); + return deviceData; + })); - return dispatch({ - type: "GET_DEVICES_DATA", - payload: { devices: devicesData.reduce((acc, curr) => ({ ...acc, ...curr }), {}) } - }); + return dispatch({ + type: "GET_DEVICES_DATA", + payload: { devices: devicesData.reduce((acc, curr) => ({ ...acc, ...curr }), {}) } + }); + } catch (error) { + return dispatch(setCriticalErrorAction(error)); + } }; }; export const getDiskSelectionAction = () => { return async (dispatch) => { - const usableDisks = await getUsableDisks(); - const diskSelection = await getAllDiskSelection(); + try { + const usableDisks = await getUsableDisks(); + const diskSelection = await getAllDiskSelection(); - return dispatch({ - type: "GET_DISK_SELECTION", - payload: { - diskSelection: { - ignoredDisks: diskSelection[0].IgnoredDisks.v, - selectedDisks: diskSelection[0].SelectedDisks.v, - usableDisks: usableDisks[0], - } - }, - }); + return dispatch({ + type: "GET_DISK_SELECTION", + payload: { + diskSelection: { + ignoredDisks: diskSelection[0].IgnoredDisks.v, + selectedDisks: diskSelection[0].SelectedDisks.v, + usableDisks: usableDisks[0], + } + }, + }); + } catch (error) { + return dispatch(setCriticalErrorAction(error)); + } }; }; export const getPartitioningDataAction = ({ requests, partitioning }) => { return async (dispatch) => { - const props = { path: partitioning }; - const convertRequests = reqs => reqs.map(request => Object.entries(request).reduce((acc, [key, value]) => ({ ...acc, [key]: value.v }), {})); + try { + const props = { path: partitioning }; + const convertRequests = reqs => reqs.map(request => Object.entries(request).reduce((acc, [key, value]) => ({ ...acc, [key]: value.v }), {})); - if (!requests) { - props.method = await getPartitioningMethod({ partitioning }); - if (props.method === "MANUAL") { - const reqs = await gatherRequests({ partitioning }); + if (!requests) { + props.method = await getPartitioningMethod({ partitioning }); + if (props.method === "MANUAL") { + const reqs = await gatherRequests({ partitioning }); - props.requests = convertRequests(reqs[0]); + props.requests = convertRequests(reqs[0]); + } + } else { + props.requests = convertRequests(requests); } - } else { - props.requests = convertRequests(requests); - } - return dispatch({ - type: "GET_PARTITIONING_DATA", - payload: { path: partitioning, partitioningData: props } - }); + return dispatch({ + type: "GET_PARTITIONING_DATA", + payload: { path: partitioning, partitioningData: props } + }); + } catch (error) { + return dispatch(setCriticalErrorAction(error)); + } }; }; diff --git a/ui/webui/src/components/app.jsx b/ui/webui/src/components/app.jsx index 1399a77e94a..c260bbd950b 100644 --- a/ui/webui/src/components/app.jsx +++ b/ui/webui/src/components/app.jsx @@ -16,7 +16,7 @@ */ import cockpit from "cockpit"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { AlertGroup, AlertVariant, AlertActionCloseButton, Alert, @@ -38,6 +38,8 @@ import { PayloadsClient } from "../apis/payloads"; import { RuntimeClient, getIsFinal } from "../apis/runtime"; import { NetworkClient, initDataNetwork, startEventMonitorNetwork } from "../apis/network.js"; +import { setCriticalErrorAction } from "../actions/miscellaneous-actions.js"; + import { readConf } from "../helpers/conf.js"; import { debug } from "../helpers/log.js"; import { useReducerWithThunk, reducer, initialState } from "../reducer.js"; @@ -47,7 +49,6 @@ const N_ = cockpit.noop; export const Application = () => { const [address, setAddress] = useState(); - const [criticalError, setCriticalError] = useState(); const [beta, setBeta] = useState(); const [conf, setConf] = useState(); const [language, setLanguage] = useState(); @@ -55,16 +56,17 @@ export const Application = () => { const [osRelease, setOsRelease] = useState(""); const [state, dispatch] = useReducerWithThunk(reducer, initialState); const [storeInitilized, setStoreInitialized] = useState(false); + const criticalError = state?.error?.criticalError; - const onCritFail = (contextData) => { - return errorHandlerWithContext(contextData, setCriticalError); - }; + const onCritFail = useCallback((contextData) => { + return errorHandlerWithContext(contextData, exc => dispatch(setCriticalErrorAction(exc))); + }, [dispatch]); useEffect(() => { // Before unload ask the user for verification window.onbeforeunload = e => ""; cockpit.file("/run/anaconda/bus.address").watch(address => { - setCriticalError(); + setCriticalErrorAction(); const clients = [ new LocalizationClient(address), new StorageClient(address), @@ -101,7 +103,7 @@ export const Application = () => { ); readOsRelease().then(osRelease => setOsRelease(osRelease)); - }, [dispatch]); + }, [dispatch, onCritFail]); const onAddNotification = (notificationProps) => { setNotifications({ diff --git a/ui/webui/src/reducer.js b/ui/webui/src/reducer.js index 6aa753cae0e..7f1e8fd378c 100644 --- a/ui/webui/src/reducer.js +++ b/ui/webui/src/reducer.js @@ -40,11 +40,17 @@ export const networkInitialState = { connected: null }; +/* Initial state for the error store substate */ +export const errorInitialState = { + criticalError: null +}; + /* Initial state for the global store */ export const initialState = { localization: localizationInitialState, storage: storageInitialState, network: networkInitialState, + error: errorInitialState, }; /* Custom hook to use the reducer with async actions */ @@ -71,6 +77,7 @@ export const reducer = (state, action) => { localization: localizationReducer(state.localization, action), storage: storageReducer(state.storage, action), network: networkReducer(state.network, action), + error: errorReducer(state.error, action) }); }; @@ -105,3 +112,11 @@ export const networkReducer = (state = networkInitialState, action) => { return state; } }; + +const errorReducer = (state = errorInitialState, action) => { + if (action.type === "SET_CRITICAL_ERROR") { + return { ...state, criticalError: action.payload.criticalError }; + } else { + return state; + } +};