Skip to content

Commit

Permalink
webui: catch exceptions from the backend in all actions
Browse files Browse the repository at this point in the history
For this let's move criticalError to the central store.
  • Loading branch information
KKoukiou committed Oct 16, 2023
1 parent fdd73fa commit ab00e3a
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 78 deletions.
61 changes: 39 additions & 22 deletions ui/webui/src/actions/localization-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
};
};
21 changes: 21 additions & 0 deletions ui/webui/src/actions/miscellaneous-actions.js
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

export const setCriticalErrorAction = (criticalError) => ({
type: "SET_CRITICAL_ERROR",
payload: { criticalError }
});
15 changes: 10 additions & 5 deletions ui/webui/src/actions/network-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
};
103 changes: 59 additions & 44 deletions ui/webui/src/actions/storage-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
};
};
16 changes: 9 additions & 7 deletions ui/webui/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand All @@ -47,24 +49,24 @@ 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();
const [notifications, setNotifications] = useState({});
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),
Expand Down Expand Up @@ -101,7 +103,7 @@ export const Application = () => {
);

readOsRelease().then(osRelease => setOsRelease(osRelease));
}, [dispatch]);
}, [dispatch, onCritFail]);

const onAddNotification = (notificationProps) => {
setNotifications({
Expand Down
15 changes: 15 additions & 0 deletions ui/webui/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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)
});
};

Expand Down Expand Up @@ -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;
}
};

0 comments on commit ab00e3a

Please sign in to comment.