Skip to content

Commit

Permalink
fix: Site build polling and latest build branch
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Burnes <[email protected]>
  • Loading branch information
apburnes committed Nov 19, 2024
1 parent f448c36 commit e478444
Show file tree
Hide file tree
Showing 21 changed files with 2,390 additions and 881 deletions.
20 changes: 1 addition & 19 deletions frontend/actions/actionCreators/buildActions.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,8 @@
const buildsFetchStartedType = 'BUILDS_FETCH_STARTED';
const buildsReceivedType = 'BUILDS_RECEIVED';
const buildRestartedType = 'BUILD_RESTARTED';

const buildsFetchStarted = () => ({
type: buildsFetchStartedType,
});

const buildsReceived = (builds) => ({
type: buildsReceivedType,
builds,
});

const buildRestarted = (build) => ({
type: buildRestartedType,
build,
});

export {
buildsFetchStarted,
buildsFetchStartedType,
buildsReceived,
buildsReceivedType,
buildRestarted,
buildRestartedType,
};
export { buildRestarted, buildRestartedType };
25 changes: 2 additions & 23 deletions frontend/actions/buildActions.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,15 @@
import api from '../util/federalistApi';
import { dispatch } from '../store';

import {
buildsFetchStarted as createBuildsFetchStartedAction,
buildsReceived as createBuildsReceivedAction,
buildRestarted as createBuildRestartedAction,
} from './actionCreators/buildActions';
import { buildRestarted } from './actionCreators/buildActions';

import alertActions from './alertActions';

const dispatchBuildsFetchStartedAction = () => {
dispatch(createBuildsFetchStartedAction());
};

const dispatchBuildsReceivedAction = (builds) => {
dispatch(createBuildsReceivedAction(builds));
};

const dispatchBuildRestartedAction = (build) => {
dispatch(createBuildRestartedAction(build));
dispatch(buildRestarted(build));
};

export default {
fetchBuilds(site) {
dispatchBuildsFetchStartedAction();
return api.fetchBuilds(site).then(dispatchBuildsReceivedAction);
},

refetchBuilds(site) {
return api.fetchBuilds(site).then(dispatchBuildsReceivedAction);
},

restartBuild(buildId, siteId) {
return api.restartBuild(buildId, siteId).then((build) => {
if (Object.keys(build).length > 0) {
Expand Down
60 changes: 60 additions & 0 deletions frontend/hooks/useBuilds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
import api from '@util/federalistApi';

// Assumes the order of builds is based on createdAt desc
export function getLatestBuildByBranch(builds) {
const latestBranches = [];

return builds.map((build) => {
if (build.completedAt && !latestBranches.includes(build.branch)) {
latestBranches.push(build.branch);
return { ...build, latestForBranch: true };
}

return { ...build, latestForBranch: false };
});
}

export function useBuilds(siteId) {
const { isPending, error, data } = useQuery({
queryKey: ['builds', parseInt(siteId, 10)],
queryFn: () =>
api.fetchBuilds({ id: siteId }).then((builds) => getLatestBuildByBranch(builds)),
refetchInterval: 10000,
refetchIntervalInBackground: false,
});

return { isPending, error, data };
}

export function useRebuild(siteId, buildId, ref) {
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: ({ buildId, siteId }) => api.restartBuild(buildId, siteId),
onSuccess: () => {
// Scroll to build history container ref
// Invalidate and refetch
return Promise.all([
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
}),
queryClient.invalidateQueries({ queryKey: ['builds', parseInt(siteId, 10)] }),
]);
},
});

async function rebuildBranch() {
return mutation.mutate({
buildId,
siteId,
});
}

return {
...mutation,
queryClient,
rebuildBranch,
};
}
85 changes: 85 additions & 0 deletions frontend/hooks/useBuilds.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { waitFor, renderHook } from '@testing-library/react';
import nock from 'nock';
import { spy } from 'sinon';
import { createTestQueryClient } from '@support/queryClient';
import { getSiteBuilds, getSiteBuildsError, postSiteBuild } from '@support/nocks';
import { useBuilds, useRebuild } from './useBuilds';

const createWrapper = createTestQueryClient();

function checkForLatest(builds) {
const hasChecked = [];

return builds.map((build) => {
const { latestForBranch, branch, completedAt } = build;

if (latestForBranch && completedAt) {
expect(!hasChecked.includes(branch)).toBe(true);
hasChecked.push(branch);
}

if (!latestForBranch && completedAt) {
expect(hasChecked.includes(branch)).toBe(true);
}
});
}

describe('useBuilds', () => {
beforeEach(() => nock.cleanAll());
afterAll(() => nock.restore());

it('should fetch builds based on site id', async () => {
const siteId = 1;
await getSiteBuilds(siteId);

const { result } = renderHook(() => useBuilds(siteId), {
wrapper: createWrapper(),
});

await waitFor(() => expect(result.current.isPending).toBe(false));

expect(Array.isArray(result.current.data)).toBe(true);
checkForLatest(result.current.data);
});

it('should return error', async () => {
const siteId = 1;
await getSiteBuildsError(siteId, 'Something happened');

const { result } = renderHook(() => useBuilds(siteId), {
wrapper: createWrapper(),
});

await waitFor(() => expect(result.current.isPending).toBe(false));
expect(result.current.error instanceof Error).toBe(true);
});

it('should mutate the builds on rebuild', async () => {
const siteId = 1;
const buildId = 1;
const ref = {
current: {
scrollIntoView: jest.fn(),
},
};

postSiteBuild(siteId, buildId);

const { result } = renderHook(() => useRebuild(siteId, buildId, ref), {
wrapper: createWrapper(),
});

const qcSpy = spy(result.current.queryClient, 'invalidateQueries');

const isFunction = result.current.rebuildBranch instanceof Function;
expect(isFunction).toBe(true);

await result.current.rebuildBranch();
await waitFor(() => expect(result.current.isSuccess).toBe(true));

expect(ref.current.scrollIntoView).toHaveBeenCalledTimes(1);
expect(qcSpy.calledOnceWith({ queryKey: ['builds', parseInt(siteId, 10)] })).toBe(
true,
);
});
});
14 changes: 9 additions & 5 deletions frontend/pages/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import PropTypes from 'prop-types';
import Notifications from 'react-notification-system-redux';
import { useSelector } from 'react-redux';
import { Outlet, useLocation } from 'react-router-dom';

import alertActions from '@actions/alertActions';
import BuildStatusNotifier from '@util/buildStatusNotifier';
import { createWrapper } from '@support/queryClient';

const { QueryClientWrapper } = createWrapper();

function shouldClearAlert(alert) {
if (alert.stale) {
Expand Down Expand Up @@ -35,10 +37,12 @@ export function App({ onEnter }) {
}, [location.key]);

return (
<div className="grid-container">
<Notifications notifications={notifications} />
<Outlet />
</div>
<QueryClientWrapper>
<div className="grid-container">
<Notifications notifications={notifications} />
<Outlet />
</div>
</QueryClientWrapper>
);
}

Expand Down
32 changes: 18 additions & 14 deletions frontend/pages/sites/$siteId/builds/Build.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';

import { useScannableBuild } from '@hooks/useScannableBuild';
import { dateAndTimeSimple, duration, timeFrom } from '@util/datetime';
import buildActions from '@actions/buildActions';
import { useRebuild } from '@hooks/useBuilds';

import GithubBuildBranchLink from '@shared/GithubBuildBranchLink';
import GithubBuildShaLink from '@shared/GithubBuildShaLink';
Expand All @@ -18,12 +18,10 @@ import {
IconReport,
IconView,
} from '@shared/icons';
import CreateScanLink from './CreateScanLink';

import { SITE, BUILD } from '@propTypes';

import CreateBuildLink from './CreateBuildLink';
import CreateScanLink from './CreateScanLink';

function BuildLogsLink({ buildId, siteId }) {
const cta = 'View build logs';
return <Link to={`/sites/${siteId}/builds/${buildId}/logs`}>{cta}</Link>;
Expand All @@ -48,11 +46,18 @@ BuildLogsLink.propTypes = {
siteId: PropTypes.number.isRequired,
};

const Build = ({ build, latestForBranch, showBuildTasks = false, site }) => {
const Build = ({
build,
containerRef,
latestForBranch,
showBuildTasks = false,
site,
}) => {
const siteId = site.id;
const buildHasBuildTasks = checkBuildHasBuildTasks(build);
const isScannableBuild = checkIsScannableBuild(build, showBuildTasks, latestForBranch);
const { isScanActionDisabled, startScan } = useScannableBuild(build);
const { isPending, rebuildBranch } = useRebuild(site.id, build.id, containerRef);

const buildStateData = ({ state, error }) => {
let messageStatusDoneIcon;
Expand Down Expand Up @@ -219,16 +224,15 @@ const Build = ({ build, latestForBranch, showBuildTasks = false, site }) => {
</p>
)}
{latestForBranch && ['error', 'success'].includes(build.state) && (
<CreateBuildLink
handlerParams={{
buildId: build.id,
siteId,
}}
handleClick={buildActions.restartBuild}
<button
type="button"
disabled={isPending}
onClick={rebuildBranch}
className="usa-button small-button margin-top-1 rebuild-button"
>
<IconRebuild />
Rebuild
</CreateBuildLink>
</button>
)}
</td>
</tr>
Expand All @@ -237,8 +241,8 @@ const Build = ({ build, latestForBranch, showBuildTasks = false, site }) => {

Build.propTypes = {
build: BUILD.isRequired,
// we're getting previewBuilds from the parent
latestForBranch: PropTypes.object.isRequired,
containerRef: PropTypes.node.isRequired,
latestForBranch: PropTypes.bool.isRequired,
showBuildTasks: PropTypes.bool,
site: SITE.isRequired,
};
Expand Down
Loading

0 comments on commit e478444

Please sign in to comment.