Skip to content

Commit

Permalink
[web] Adapt storage components
Browse files Browse the repository at this point in the history
  • Loading branch information
joseivanlopez committed Sep 4, 2023
1 parent 9baa603 commit a61e419
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 206 deletions.
11 changes: 7 additions & 4 deletions web/src/components/overview/StorageSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ const ProposalSummary = ({ proposal }) => {

if (result === undefined) return <Text>{_("Device not selected yet")}</Text>;

const [candidateDevice] = result.candidateDevices;
const device = proposal.availableDevices.find(d => d.name === candidateDevice);
const bootDevice = result.settings.bootDevice;
const device = proposal.availableDevices.find(d => d.name === bootDevice);

const label = device ? deviceLabel(device) : candidateDevice;
const label = device ? deviceLabel(device) : bootDevice;

// TRANSLATORS: %s will be replaced by the device name and its size,
// example: "/dev/sda, 20 GiB"
Expand Down Expand Up @@ -108,7 +108,10 @@ export default function StorageSection({ showErrors = false }) {
const isDeprecated = await cancellablePromise(client.isDeprecated());
if (isDeprecated) await cancellablePromise(client.probe());

const proposal = await cancellablePromise(client.proposal.getData());
const proposal = {
availableDevices: await cancellablePromise(client.proposal.getAvailableDevices()),
result: await cancellablePromise(client.proposal.getResult())
};
const issues = await cancellablePromise(client.getErrors());
const errors = issues.map(toValidationError);

Expand Down
28 changes: 18 additions & 10 deletions web/src/components/overview/StorageSection.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,32 @@ jest.mock("~/client");
jest.mock("~/components/core/SectionSkeleton", () => mockComponent("Loading storage"));

let status = IDLE;
let proposal = {
availableDevices: [
{ name: "/dev/sda", size: 536870912000 },
{ name: "/dev/sdb", size: 697932185600 }
],
result: {
candidateDevices: ["/dev/sda"],

const availableDevices = [
{ name: "/dev/sda", size: 536870912000 },
{ name: "/dev/sdb", size: 697932185600 }
];

let proposalResult = {
settings: {
bootDevice: "/dev/sda",
lvm: false
}
},
actions: []
};

let errors = [];

let onStatusChangeFn = jest.fn();

beforeEach(() => {
createClient.mockImplementation(() => {
return {
storage: {
proposal: { getData: jest.fn().mockResolvedValue(proposal) },
proposal: {
getAvailableDevices: jest.fn().mockResolvedValue(availableDevices),
getResult: jest.fn().mockResolvedValue(proposalResult),
},
getStatus: jest.fn().mockResolvedValue(status),
getProgress: jest.fn().mockResolvedValue({
message: "Activating storage devices", current: 1, total: 4
Expand Down Expand Up @@ -114,7 +122,7 @@ describe("when there is a proposal", () => {

describe("when there is no proposal yet", () => {
beforeEach(() => {
proposal = { result: undefined };
proposalResult = undefined;
errors = [{ description: "Fake error" }];
});

Expand Down
92 changes: 66 additions & 26 deletions web/src/components/storage/ProposalPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,25 +49,31 @@ const reducer = (state, action) => {
return { ...state, loading: false };
}

case "UPDATE_PROPOSAL": {
const { proposal, errors } = action.payload;
const { availableDevices, volumeTemplates, result = {} } = proposal;
const { candidateDevices, lvm, encryptionPassword, volumes, actions } = result;
return {
...state,
availableDevices,
volumeTemplates,
settings: { candidateDevices, lvm, encryptionPassword, volumes },
actions,
errors
};
case "UPDATE_AVAILABLE_DEVICES": {
const { availableDevices } = action.payload;
return { ...state, availableDevices };
}

case "UPDATE_VOLUME_TEMPLATES": {
const { volumeTemplates } = action.payload;
return { ...state, volumeTemplates };
}

case "UPDATE_RESULT": {
const { settings, actions } = action.payload.result;
return { ...state, settings, actions };
}

case "UPDATE_SETTINGS": {
const { settings } = action.payload;
return { ...state, settings };
}

case "UPDATE_ERRORS": {
const { errors } = action.payload;
return { ...state, errors };
}

default: {
return state;
}
Expand All @@ -79,11 +85,29 @@ export default function ProposalPage() {
const { cancellablePromise } = useCancellablePromise();
const [state, dispatch] = useReducer(reducer, initialState);

const loadProposal = useCallback(async () => {
const proposal = await cancellablePromise(client.proposal.getData());
const loadAvailableDevices = useCallback(async () => {
return await cancellablePromise(client.proposal.getAvailableDevices());
}, [client, cancellablePromise]);

const loadVolumeTemplates = useCallback(async () => {
const mountPoints = await cancellablePromise(client.proposal.getProductMountPoints());
const volumeTemplates = [];

for (const mountPoint of mountPoints) {
volumeTemplates.push(await cancellablePromise(client.proposal.defaultVolume(mountPoint)));
}

volumeTemplates.push(await cancellablePromise(client.proposal.defaultVolume("")));
return volumeTemplates;
}, [client, cancellablePromise]);

const loadProposalResult = useCallback(async () => {
return await cancellablePromise(client.proposal.getResult());
}, [client, cancellablePromise]);

const loadErrors = useCallback(async () => {
const issues = await cancellablePromise(client.getErrors());
const errors = issues.map(toValidationError);
return { proposal, errors };
return issues.map(toValidationError);
}, [client, cancellablePromise]);

const load = useCallback(async () => {
Expand All @@ -92,20 +116,34 @@ export default function ProposalPage() {
const isDeprecated = await cancellablePromise(client.isDeprecated());
if (isDeprecated) await client.probe();

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

const volumeTemplates = await loadVolumeTemplates();
dispatch({ type: "UPDATE_VOLUME_TEMPLATES", payload: { volumeTemplates } });

const result = await loadProposalResult();
if (result !== undefined) dispatch({ type: "UPDATE_RESULT", payload: { result } });

const errors = await loadErrors();
dispatch({ type: "UPDATE_ERRORS", payload: { errors } });

if (result !== undefined) dispatch({ type: "STOP_LOADING" });
}, [cancellablePromise, client, loadAvailableDevices, loadErrors, loadProposalResult, loadVolumeTemplates]);

const calculate = useCallback(async (settings) => {
dispatch({ type: "START_LOADING" });

await cancellablePromise(client.proposal.calculate(settings));

const { proposal, errors } = await loadProposal();
dispatch({ type: "UPDATE_PROPOSAL", payload: { proposal, errors } });
const result = await loadProposalResult();
dispatch({ type: "UPDATE_RESULT", payload: { result } });

const errors = await loadErrors();
dispatch({ type: "UPDATE_ERRORS", payload: { errors } });

dispatch({ type: "STOP_LOADING" });
}, [cancellablePromise, client, loadProposal]);
}, [cancellablePromise, client, loadErrors, loadProposalResult]);

useEffect(() => {
load().catch(console.error);
Expand All @@ -114,7 +152,7 @@ export default function ProposalPage() {
}, [client, load]);

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

const statusHandler = (serviceStatus) => {
// Load the proposal if no proposal has been loaded yet. This can happen if the proposal
Expand All @@ -138,8 +176,10 @@ export default function ProposalPage() {
// Templates for already existing mount points are filtered out
const usefulTemplates = () => {
const volumes = state.settings.volumes || [];
const mountPoints = volumes.map(v => v.mountPoint);
return state.volumeTemplates.filter(t => !mountPoints.includes(t.mountPoint));
const mountPaths = volumes.map(v => v.mountPath);
return state.volumeTemplates.filter(t => (
t.mountPath.length > 0 && !mountPaths.includes(t.mountPath)
));
};

return (
Expand Down
96 changes: 47 additions & 49 deletions web/src/components/storage/ProposalPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,61 +38,44 @@ jest.mock("@patternfly/react-core", () => {
};
});

const defaultProposalData = {
availableDevices: [],
volumeTemplates: [],
result: {
candidateDevices: ["/dev/vda"],
lvm: false,
encryptionPassword: "",
volumes: []
}
const storageMock = {
probe: jest.fn().mockResolvedValue(0),
proposal: {
getAvailableDevices: jest.fn().mockResolvedValue([]),
getProductMountPoints: jest.fn().mockResolvedValue([]),
getResult: jest.fn().mockResolvedValue(undefined),
defaultVolume: jest.fn(mountPath => Promise.resolve({ mountPath })),
calculate: jest.fn().mockResolvedValue(0)
},
getErrors: jest.fn().mockResolvedValue([]),
isDeprecated: jest.fn().mockResolvedValue(false),
onDeprecate: jest.fn(),
onStatusChange: jest.fn()
};

let proposalData;

const probeFn = jest.fn().mockResolvedValue(0);

const isDeprecatedFn = jest.fn();

let onDeprecateFn = jest.fn();

let onStatusChangeFn = jest.fn();
let storage;

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

proposalData = { ...defaultProposalData };

createClient.mockImplementation(() => {
return {
storage: {
probe: probeFn,
proposal: {
getData: jest.fn().mockResolvedValue(proposalData),
calculate: jest.fn().mockResolvedValue(0)
},
getErrors: jest.fn().mockResolvedValue([]),
isDeprecated: isDeprecatedFn,
onDeprecate: onDeprecateFn,
onStatusChange: onStatusChangeFn
}
};
});
storage = { ...storageMock, proposal: { ...storageMock.proposal } };
createClient.mockImplementation(() => ({ storage }));
});

it("probes storage if the storage devices are deprecated", async () => {
isDeprecatedFn.mockResolvedValue(true);
storage.isDeprecated = jest.fn().mockResolvedValue(true);
installerRender(<ProposalPage />);
await waitFor(() => expect(probeFn).toHaveBeenCalled());
await waitFor(() => expect(storage.probe).toHaveBeenCalled());
});

it("does not probe storage if the storage devices are not deprecated", async () => {
installerRender(<ProposalPage />);
await waitFor(() => expect(probeFn).not.toHaveBeenCalled());
await waitFor(() => expect(storage.probe).not.toHaveBeenCalled());
});

it("loads the proposal data", async () => {
storage.proposal.getResult = jest.fn().mockResolvedValue(
{ settings: { bootDevice: "/dev/vda" } }
);

installerRender(<ProposalPage />);

screen.getAllByText(/PFSkeleton/);
Expand All @@ -117,24 +100,29 @@ it("renders the settings and actions sections", async () => {
describe("when the storage devices become deprecated", () => {
it("probes storage", async () => {
const [mockFunction, callbacks] = createCallbackMock();
onDeprecateFn = mockFunction;
storage.onDeprecate = mockFunction;

installerRender(<ProposalPage />);

isDeprecatedFn.mockResolvedValue(true);
storage.isDeprecated = jest.fn().mockResolvedValue(true);
const [onDeprecateCb] = callbacks;
await act(() => onDeprecateCb());

await waitFor(() => expect(probeFn).toHaveBeenCalled());
await waitFor(() => expect(storage.probe).toHaveBeenCalled());
});

it("loads the proposal data", async () => {
const result = { settings: { bootDevice: "/dev/vda" } };
storage.proposal.getResult = jest.fn().mockResolvedValue(result);

const [mockFunction, callbacks] = createCallbackMock();
onDeprecateFn = mockFunction;
storage.onDeprecate = mockFunction;

installerRender(<ProposalPage />);

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

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

const [onDeprecateCb] = callbacks;
await act(() => onDeprecateCb());
Expand All @@ -145,7 +133,7 @@ describe("when the storage devices become deprecated", () => {

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

it("shows the page as loading", async () => {
Expand All @@ -157,12 +145,15 @@ describe("when there is no proposal yet", () => {

it("loads the proposal when the service finishes to calculate", async () => {
const [mockFunction, callbacks] = createCallbackMock();
onStatusChangeFn = mockFunction;
storage.onStatusChange = mockFunction;

installerRender(<ProposalPage />);

screen.getAllByText(/PFSkeleton/);

proposalData.result = { ...defaultProposalData.result };
storage.proposal.getResult = jest.fn().mockResolvedValue(
{ settings: { bootDevice: "/dev/vda" } }
);

const [onStatusChangeCb] = callbacks;
await act(() => onStatusChangeCb(IDLE));
Expand All @@ -171,9 +162,16 @@ describe("when there is no proposal yet", () => {
});

describe("when there is a proposal", () => {
beforeEach(() => {
storage.proposal.getResult = jest.fn().mockResolvedValue(
{ settings: { bootDevice: "/dev/vda" } }
);
});

it("does not load the proposal when the service finishes to calculate", async () => {
const [mockFunction, callbacks] = createCallbackMock();
onStatusChangeFn = mockFunction;
storage.proposal.onStatusChange = mockFunction;

installerRender(<ProposalPage />);

await screen.findByText("/dev/vda");
Expand Down
Loading

0 comments on commit a61e419

Please sign in to comment.