Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webui: catch exceptions from the backend in all actions #5251

Merged
merged 1 commit into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
};