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

Upgradable packages view #2668

Merged
merged 1 commit into from
Jun 6, 2024
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
15 changes: 5 additions & 10 deletions assets/js/common/UpgradablePackagesList/UpgradablePackagesList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ function UpgradablePackagesList({
key: 'patches',
render: (content, { to_package_id }) => (
<div>
{content.map(({ advisory_name }) => (
<div key={`${to_package_id}-${advisory_name}`}>
{advisory_name}
</div>
))}
{content &&
content.map(({ advisory }) => (
<div key={`${to_package_id}-${advisory}`}>{advisory}</div>
))}
</div>
),
},
Expand All @@ -47,11 +46,7 @@ function UpgradablePackagesList({
};
});

return (
<div className="bg-white rounded-lg shadow">
<Table config={config} data={data} />
</div>
);
return <Table config={config} data={data} />;
}

export default UpgradablePackagesList;
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ describe('UpgradablePackagesList component', () => {

expect(screen.getByText(expectedInstalledPackage)).toBeVisible();
expect(screen.getByText(expectedLatestPackage)).toBeVisible();
patches.forEach(({ advisory_name }) => {
expect(screen.getByText(advisory_name)).toBeVisible();
patches.forEach(({ advisory }) => {
expect(screen.getByText(advisory)).toBeVisible();
});
});
});
3 changes: 3 additions & 0 deletions assets/js/common/UpgradablePackagesList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import UpgradablePackagesList from './UpgradablePackagesList';

export default UpgradablePackagesList;
5 changes: 5 additions & 0 deletions assets/js/lib/api/softwareUpdates.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ import { networkClient } from '@lib/network';

export const getSoftwareUpdates = (hostID) =>
networkClient.get(`/hosts/${hostID}/software_updates`);

export const getPatchesForPackages = (packageIDs) =>
networkClient.get(`/software_updates/packages`, {
params: { package_ids: packageIDs },
});
9 changes: 9 additions & 0 deletions assets/js/lib/test-utils/factories/relevantPatches.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ export const relevantPatchFactory = Factory.define(() => ({
date: faker.date.anytime(),
update_date: faker.date.anytime(),
}));

export const patchForPackageFactory = Factory.define(() => ({
advisory: faker.animal.cat(),
type: faker.helpers.arrayElement(advisoryType),
synopsis: faker.lorem.sentence(),
issue_date: faker.date.anytime().toString(),
update_date: faker.date.anytime().toString(),
last_modified_date: faker.date.anytime().toString(),
}));
4 changes: 2 additions & 2 deletions assets/js/lib/test-utils/factories/upgradablePackage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { faker } from '@faker-js/faker';
import { Factory } from 'fishery';
import { relevantPatchFactory } from './relevantPatches';
import { patchForPackageFactory } from './relevantPatches';

const releaseVersionFactory = () =>
`${faker.number.int({ min: 100000, max: 160000 })}.${faker.system.semver()}`;
Expand All @@ -17,5 +17,5 @@ export const upgradablePackageFactory = Factory.define(() => ({
to_version: faker.system.semver(),
from_arch: faker.airline.flightNumber(),
to_arch: faker.airline.flightNumber(),
patches: relevantPatchFactory.buildList(2),
patches: patchForPackageFactory.buildList(2),
}));
44 changes: 44 additions & 0 deletions assets/js/pages/UpgradablePackagesPage/UpgradablePackagesPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';

import { getUpgradablePackages } from '@state/selectors/softwareUpdates';
import { fetchSoftwareUpdatesSettings } from '@state/softwareUpdatesSettings';
import {
fetchSoftwareUpdates,
fetchUpgradablePackagesPatches,
} from '@state/softwareUpdates';

import BackButton from '@common/BackButton';
import UpgradablePackagesList from '@common/UpgradablePackagesList';

function UpgradablePackagesPage() {
const { hostID } = useParams();
const dispatch = useDispatch();

useEffect(() => {
dispatch(fetchSoftwareUpdates(hostID));
dispatch(fetchSoftwareUpdatesSettings());
}, []);

const upgradablePackages = useSelector((state) =>
getUpgradablePackages(state, hostID)
);

useEffect(() => {
const packageIDs = upgradablePackages.map(
({ to_package_id: packageID }) => packageID
);

dispatch(fetchUpgradablePackagesPatches({ hostID, packageIDs }));
}, [upgradablePackages.length, hostID]);

return (
<>
<BackButton url={`/hosts/${hostID}`}>Back to Host Details</BackButton>
<UpgradablePackagesList upgradablePackages={upgradablePackages} />
</>
);
}

export default UpgradablePackagesPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { screen } from '@testing-library/react';

import { faker } from '@faker-js/faker';

import {
renderWithRouterMatch,
withState,
defaultInitialState,
} from '@lib/test-utils';
import { upgradablePackageFactory } from '@lib/test-utils/factories/upgradablePackage';
import { patchForPackageFactory } from '@lib/test-utils/factories/relevantPatches';

import UpgradablePackagesPage from './UpgradablePackagesPage';

describe('UpgradablePackagesPage', () => {
it('should render correctly', () => {
const hostID = faker.string.uuid();
const patch = patchForPackageFactory.build();
const upgradablePackages = upgradablePackageFactory.buildList(10, {
patches: [patch],
});
const [{ name }] = upgradablePackages;

const [StatefulPage] = withState(<UpgradablePackagesPage />, {
...defaultInitialState,
softwareUpdates: {
softwareUpdates: {
[hostID]: {
loading: false,
errors: [],
upgradable_packages: upgradablePackages,
},
},
},
});

renderWithRouterMatch(StatefulPage, {
path: 'hosts/:hostID/packages',
route: `/hosts/${hostID}/packages`,
});

expect(screen.getAllByText(name, { exact: false })).toHaveLength(2);
});
});
3 changes: 3 additions & 0 deletions assets/js/pages/UpgradablePackagesPage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import UpgradablePackagesPage from './UpgradablePackagesPage';

export default UpgradablePackagesPage;
24 changes: 23 additions & 1 deletion assets/js/state/sagas/softwareUpdates.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { get } from 'lodash';
import { put, call, takeEvery } from 'redux-saga/effects';
import { getSoftwareUpdates } from '@lib/api/softwareUpdates';
import {
getSoftwareUpdates,
getPatchesForPackages,
} from '@lib/api/softwareUpdates';

import {
FETCH_UPGRADABLE_PACKAGES_PATCHES,
FETCH_SOFTWARE_UPDATES,
startLoadingSoftwareUpdates,
setSoftwareUpdates,
setEmptySoftwareUpdates,
setSoftwareUpdatesErrors,
setPatchesForPackages,
} from '@state/softwareUpdates';

export function* fetchSoftwareUpdates({ payload: hostID }) {
Expand All @@ -24,6 +29,23 @@ export function* fetchSoftwareUpdates({ payload: hostID }) {
}
}

export function* fetchUpgradablePackagesPatches({
payload: { hostID, packageIDs },
}) {
try {
const {
data: { patches },
} = yield call(getPatchesForPackages, packageIDs);
yield put(setPatchesForPackages({ hostID, patches }));
} catch (error) {
yield put(setPatchesForPackages({ hostID, patches: [] }));
}
}

export function* watchSoftwareUpdates() {
yield takeEvery(FETCH_SOFTWARE_UPDATES, fetchSoftwareUpdates);
yield takeEvery(
FETCH_UPGRADABLE_PACKAGES_PATCHES,
fetchUpgradablePackagesPatches
);
}
32 changes: 31 additions & 1 deletion assets/js/state/sagas/softwareUpdates.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker';
import MockAdapter from 'axios-mock-adapter';

import { recordSaga } from '@lib/test-utils';
import { patchForPackageFactory } from '@lib/test-utils/factories';

import { networkClient } from '@lib/network';

Expand All @@ -10,9 +11,13 @@ import {
setSoftwareUpdates,
setSoftwareUpdatesErrors,
setEmptySoftwareUpdates,
setPatchesForPackages,
} from '@state/softwareUpdates';

import { fetchSoftwareUpdates } from './softwareUpdates';
import {
fetchSoftwareUpdates,
fetchUpgradablePackagesPatches,
} from './softwareUpdates';

describe('Software Updates saga', () => {
describe('Fetching Software Updates', () => {
Expand Down Expand Up @@ -87,4 +92,29 @@ describe('Software Updates saga', () => {
}
);
});

describe('Fetching patches for packages', () => {
it('sets patches for upgradable packages', async () => {
const axiosMock = new MockAdapter(networkClient);
const hostID = faker.string.uuid();
const packageIDs = [faker.string.uuid(), faker.string.uuid()];
const patches = patchForPackageFactory.buildList(3);
const response = {
patches: [
{ package_id: packageIDs[0], patches },
{ package_id: packageIDs[1], patches },
],
};

axiosMock.onGet(`/api/v1/software_updates/packages`).reply(200, response);

const dispatched = await recordSaga(fetchUpgradablePackagesPatches, {
payload: { hostID, packageIDs },
});

expect(dispatched).toEqual([
setPatchesForPackages({ hostID, patches: response.patches }),
]);
});
});
});
5 changes: 5 additions & 0 deletions assets/js/state/selectors/softwareUpdates.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export const getSoftwareUpdatesPatches = createSelector(
(softwareUpdates) => get(softwareUpdates, ['relevant_patches'], [])
);

export const getUpgradablePackages = createSelector(
[(state, id) => getSoftwareUpdatesForHost(id)(state)],
(softwareUpdates) => get(softwareUpdates, ['upgradable_packages'], [])
);

export const getSoftwareUpdatesLoading = createSelector(
[(state, id) => getSoftwareUpdatesForHost(id)(state)],
(softwareUpdates) => get(softwareUpdates, ['loading'], false)
Expand Down
7 changes: 7 additions & 0 deletions assets/js/state/selectors/softwareUpdates.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getSoftwareUpdatesStats,
getSoftwareUpdatesLoading,
getSoftwareUpdatesPatches,
getUpgradablePackages,
} from './softwareUpdates';

describe('Software Updates selector', () => {
Expand Down Expand Up @@ -134,4 +135,10 @@ describe('Software Updates selector', () => {
softwareUpdates[hostID].relevant_patches
);
});

it('should return the upgradable packages', () => {
expect(getUpgradablePackages(state, hostID)).toEqual(
softwareUpdates[hostID].upgradable_packages
);
});
});
30 changes: 30 additions & 0 deletions assets/js/state/softwareUpdates.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createAction, createSlice } from '@reduxjs/toolkit';
import { find, get, isEmpty } from 'lodash';

const initialState = {
softwareUpdates: {},
Expand Down Expand Up @@ -44,18 +45,47 @@ export const softwareUpdatesSlice = createSlice({
[hostID]: { ...initialHostState, errors },
};
},
setPatchesForPackages: (state, { payload: { hostID, patches } }) => {
const packages = get(
state,
['softwareUpdates', hostID, 'upgradable_packages'],
[]
);

const newPackages = packages.map((currentPackage) => {
const { to_package_id: packageID } = currentPackage;

const packageInfo = find(patches, { package_id: packageID });
const packagePatches = get(packageInfo, 'patches', []);

return {
...currentPackage,
patches: packagePatches,
};
});

if (!isEmpty(packages)) {
state.softwareUpdates[hostID].upgradable_packages = newPackages;
}
},
},
});

export const FETCH_SOFTWARE_UPDATES = 'FETCH_SOFTWARE_UPDATES';
export const FETCH_UPGRADABLE_PACKAGES_PATCHES =
'FETCH_UPGRADABLE_PACKAGES_PATCHES';

export const fetchSoftwareUpdates = createAction(FETCH_SOFTWARE_UPDATES);
export const fetchUpgradablePackagesPatches = createAction(
FETCH_UPGRADABLE_PACKAGES_PATCHES
);

export const {
startLoadingSoftwareUpdates,
setSoftwareUpdates,
setEmptySoftwareUpdates,
setSoftwareUpdatesErrors,
setPatchesForPackages,
} = softwareUpdatesSlice.actions;

export default softwareUpdatesSlice.reducer;
Loading