Skip to content
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
4 changes: 2 additions & 2 deletions web/src/client/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class ProposalManager {
* @typedef {object} ProposalData
* @property {AvailableDevice[]} availableDevices
* @property {Volume[]} volumeTemplates
* @property {Result} result
* @property {Result|undefined} result
*/
async getData() {
const availableDevices = await this.getAvailableDevices();
Expand Down Expand Up @@ -154,7 +154,7 @@ class ProposalManager {
/**
* Gets the values of the current proposal
*
* @return {Promise<Result>}
* @return {Promise<Result|undefined>}
*/
async getResult() {
const proxy = await this.proposalProxy();
Expand Down
19 changes: 17 additions & 2 deletions web/src/components/storage/ProposalPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { toValidationError, useCancellablePromise } from "~/utils";
import { Icon } from "~/components/layout";
import { Page } from "~/components/core";
import { ProposalActionsSection, ProposalPageOptions, ProposalSettingsSection } from "~/components/storage";
import { IDLE } from "~/client/status";

const initialState = {
loading: true,
Expand All @@ -49,7 +50,7 @@ const reducer = (state, action) => {

case "UPDATE_PROPOSAL": {
const { proposal, errors } = action.payload;
const { availableDevices, volumeTemplates, result } = proposal;
const { availableDevices, volumeTemplates, result = {} } = proposal;
const { candidateDevices, lvm, encryptionPassword, volumes, actions } = result;
return {
...state,
Expand Down Expand Up @@ -92,7 +93,7 @@ export default function ProposalPage() {

const { proposal, errors } = await loadProposal();
dispatch({ type: "UPDATE_PROPOSAL", payload: { proposal, errors } });
dispatch({ type: "STOP_LOADING" });
if (proposal.result !== undefined) dispatch({ type: "STOP_LOADING" });
}, [cancellablePromise, client, loadProposal]);

const calculate = useCallback(async (settings) => {
Expand All @@ -111,6 +112,20 @@ export default function ProposalPage() {
return client.onDeprecate(() => load());
}, [client, load]);

useEffect(() => {
const proposalLoaded = () => state.settings.candidateDevices !== undefined;

const statusHandler = (serviceStatus) => {
// Load the proposal if no proposal has been loaded yet. This can happen if the proposal
// page is visited before probing has finished.
if (serviceStatus === IDLE && !proposalLoaded()) load();
};

if (!proposalLoaded()) {
return client.onStatusChange(statusHandler);
}
}, [client, load, state.settings]);

const changeSettings = async (settings) => {
const newSettings = { ...state.settings, ...settings };

Expand Down
50 changes: 47 additions & 3 deletions web/src/components/storage/ProposalPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import React from "react";
import { act, screen, waitFor } from "@testing-library/react";
import { createCallbackMock, installerRender, mockComponent } from "~/test-utils";
import { createClient } from "~/client";
import { IDLE } from "~/client/status";
import { ProposalPage } from "~/components/storage";

jest.mock("~/client");
Expand Down Expand Up @@ -56,10 +57,12 @@ const isDeprecatedFn = jest.fn();

let onDeprecateFn = jest.fn();

let onStatusChangeFn = jest.fn();

beforeEach(() => {
isDeprecatedFn.mockResolvedValue(false);

proposalData = defaultProposalData;
proposalData = { ...defaultProposalData };

createClient.mockImplementation(() => {
return {
Expand All @@ -71,7 +74,8 @@ beforeEach(() => {
},
getErrors: jest.fn().mockResolvedValue([]),
isDeprecated: isDeprecatedFn,
onDeprecate: onDeprecateFn
onDeprecate: onDeprecateFn,
onStatusChange: onStatusChangeFn
}
};
});
Expand Down Expand Up @@ -130,11 +134,51 @@ describe("when the storage devices become deprecated", () => {

await screen.findByText("/dev/vda");

proposalData.result.candidateDevices = ["/dev/vdb"];
proposalData.result = { ...defaultProposalData.result, candidateDevices: ["/dev/vdb"] };

const [onDeprecateCb] = callbacks;
await act(() => onDeprecateCb());

await screen.findByText("/dev/vdb");
});
});

describe("when there is no proposal yet", () => {
beforeEach(() => {
proposalData.result = undefined;
});

it("shows the page as loading", async () => {
installerRender(<ProposalPage />);

screen.getAllByText(/PFSkeleton/);
await waitFor(() => expect(screen.queryByText(/Installation device/)).toBeNull());
});

it("loads the proposal when the service finishes to calculate", async () => {
const [mockFunction, callbacks] = createCallbackMock();
onStatusChangeFn = mockFunction;
installerRender(<ProposalPage />);

screen.getAllByText(/PFSkeleton/);

proposalData.result = { ...defaultProposalData.result };

const [onStatusChangeCb] = callbacks;
await act(() => onStatusChangeCb(IDLE));
await screen.findByText("/dev/vda");
});
});

describe("when there is a proposal", () => {
it("does not load the proposal when the service finishes to calculate", async () => {
const [mockFunction, callbacks] = createCallbackMock();
onStatusChangeFn = mockFunction;
installerRender(<ProposalPage />);

await screen.findByText("/dev/vda");

const [onStatusChangeCb] = callbacks;
expect(onStatusChangeCb).toBeUndefined();
});
});