Skip to content

Commit

Permalink
Fetch jurisdictions discrepancy counts from separate endpoint (#1900)
Browse files Browse the repository at this point in the history
Previously, discrepancy counts were included in jurisdiction round
status. This can be a slow calculation, since we need to load all the
sampled ballot interpretations. Since the jurisdictions endpoint blocks
page load, it's not great to have a slow calculation as part of that
endpoint. Instead, we load the discrepancy counts separately and show a
spinner while they are loading.
  • Loading branch information
jonahkagan authored Mar 18, 2024
1 parent 251d35f commit f4abffb
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 181 deletions.
33 changes: 30 additions & 3 deletions client/src/components/AuditAdmin/Progress/Progress.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,14 @@ describe('Progress screen', () => {
})

it('shows round status for ballot comparison', async () => {
const expectedCalls = [aaApiCalls.getMapData]
const expectedCalls = [
aaApiCalls.getMapData,
aaApiCalls.getDiscrepancyCounts({
[jurisdictionMocks.allComplete[0].id]: 0,
[jurisdictionMocks.allComplete[1].id]: 2,
[jurisdictionMocks.allComplete[2].id]: 1,
}),
]
await withMockFetch(expectedCalls, async () => {
const { container } = render({
auditSettings: auditSettingsMocks.ballotComparisonAll,
Expand All @@ -249,6 +256,10 @@ describe('Progress screen', () => {

expect(container.querySelectorAll('.d3-component').length).toBe(1)

// One spinner for each jurisdiction's discrepancy count
expect(container.querySelectorAll('.bp3-spinner').length).toBe(
1 + jurisdictionMocks.allComplete.length
)
await waitFor(() => {
expect(container.querySelectorAll('.bp3-spinner').length).toBe(0)
})
Expand Down Expand Up @@ -315,7 +326,14 @@ describe('Progress screen', () => {
})

it('shows round status for batch comparison', async () => {
const expectedCalls = [aaApiCalls.getMapData]
const expectedCalls = [
aaApiCalls.getMapData,
aaApiCalls.getDiscrepancyCounts({
[jurisdictionMocks.oneComplete[0].id]: 3,
[jurisdictionMocks.oneComplete[1].id]: 2,
[jurisdictionMocks.oneComplete[2].id]: 1,
}),
]
await withMockFetch(expectedCalls, async () => {
const { container } = render({
auditSettings: auditSettingsMocks.batchComparisonAll,
Expand Down Expand Up @@ -346,13 +364,15 @@ describe('Progress screen', () => {
expect(row1[0]).toHaveTextContent('Jurisdiction 1')
expectStatusTag(row1[1], 'In progress', 'warning')
expect(row1[2]).toHaveTextContent('2,117')
// Discrepancies hidden until jurisdiction is complete
expect(row1[3]).toHaveTextContent('')
expect(row1[4]).toHaveTextContent('4')
expect(row1[5]).toHaveTextContent('6')
const row2 = within(rows[2]).getAllByRole('cell')
expect(row2[0]).toHaveTextContent('Jurisdiction 2')
expectStatusTag(row2[1], 'Not started', 'none')
expect(row2[2]).toHaveTextContent('2,117')
// Discrepancies hidden until jurisdiction is complete
expect(row2[3]).toHaveTextContent('')
expect(row2[4]).toHaveTextContent('0')
expect(row2[5]).toHaveTextContent('0')
Expand Down Expand Up @@ -595,7 +615,14 @@ describe('Progress screen', () => {
})

it('shows a different toggle label for batch audits', async () => {
const expectedCalls = [aaApiCalls.getMapData]
const expectedCalls = [
aaApiCalls.getMapData,
aaApiCalls.getDiscrepancyCounts({
[jurisdictionMocks.oneComplete[0].id]: 0,
[jurisdictionMocks.oneComplete[1].id]: 0,
[jurisdictionMocks.oneComplete[2].id]: 0,
}),
]
await withMockFetch(expectedCalls, async () => {
const { container } = render({
jurisdictions: jurisdictionMocks.oneComplete,
Expand Down
37 changes: 27 additions & 10 deletions client/src/components/AuditAdmin/Progress/Progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
ITagProps,
Icon,
AnchorButton,
Spinner,
} from '@blueprintjs/core'
import H2Title from '../../Atoms/H2Title'
import {
JurisdictionRoundStatus,
IJurisdiction,
getJurisdictionStatus,
JurisdictionProgressStatus,
useDiscrepancyCountsByJurisdiction,
} from '../../useJurisdictions'
import JurisdictionDetail from './JurisdictionDetail'
import {
Expand Down Expand Up @@ -69,6 +71,14 @@ const Progress: React.FC<IProgressProps> = ({
round,
}: IProgressProps) => {
const { electionId } = useParams<{ electionId: string }>()
const { auditType } = auditSettings
const showDiscrepancies =
Boolean(round) &&
(auditType === 'BALLOT_COMPARISON' || auditType === 'BATCH_COMPARISON')
const discrepancyCountsQuery = useDiscrepancyCountsByJurisdiction(
electionId,
{ enabled: showDiscrepancies }
)
// Store sort and filter state in URL search params to allow it to persist
// across page refreshes
const [sortAndFilterState, setSortAndFilterState] = useSearchParams<{
Expand All @@ -81,17 +91,12 @@ const Progress: React.FC<IProgressProps> = ({
string | null
>(null)

const { auditType } = auditSettings

const ballotsOrBatches =
auditType === 'BATCH_COMPARISON' ? 'Batches' : 'Ballots'

const showDiscrepancies =
round &&
(auditType === 'BALLOT_COMPARISON' || auditType === 'BATCH_COMPARISON')
const someJurisdictionHasDiscrepancies = jurisdictions.some(
jurisdiction => (jurisdiction.currentRoundStatus?.numDiscrepancies || 0) > 0
)
const someJurisdictionHasDiscrepancies = Object.values(
discrepancyCountsQuery.data ?? {}
).some(count => count > 0)

const columns: Column<IJurisdiction>[] = [
{
Expand Down Expand Up @@ -306,16 +311,28 @@ const Progress: React.FC<IProgressProps> = ({
if (showDiscrepancies) {
columns.push({
Header: 'Discrepancies',
accessor: ({ currentRoundStatus: s }) => s && s.numDiscrepancies,
accessor: ({ id, currentRoundStatus: s }) =>
s &&
s.status === JurisdictionRoundStatus.COMPLETE &&
discrepancyCountsQuery.data?.[id],
Cell: ({ value }: { value: number | null }) => {
if (discrepancyCountsQuery.isLoading) {
return (
<div style={{ display: 'flex', justifyContent: 'start' }}>
<Spinner size={Spinner.SIZE_SMALL} />
</div>
)
}
if (!value) return null
return (
<>
<Icon icon="flag" intent="danger" /> {value.toLocaleString()}
</>
)
},
Footer: totalFooter('Discrepancies'),
Footer: discrepancyCountsQuery.isLoading
? () => null
: totalFooter('Discrepancies'),
})
}
columns.push(
Expand Down
14 changes: 5 additions & 9 deletions client/src/components/_mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
IBallotManifestInfo,
IBatchTalliesFileInfo,
IJurisdiction,
DiscrepancyCountsByJurisdiction,
} from './useJurisdictions'
import { IStandardizedContest } from './useStandardizedContests'
import { ISampleSizesResponse } from './AuditAdmin/Setup/Review/useSampleSizes'
Expand Down Expand Up @@ -686,7 +687,6 @@ export const jurisdictionMocks = mocksOfType<IJurisdiction[]>()({
numUnique: 10,
numSamplesAudited: 5,
numSamples: 11,
numDiscrepancies: null,
},
},
{
Expand All @@ -701,7 +701,6 @@ export const jurisdictionMocks = mocksOfType<IJurisdiction[]>()({
numUnique: 20,
numSamplesAudited: 0,
numSamples: 22,
numDiscrepancies: null,
},
},
{
Expand All @@ -716,7 +715,6 @@ export const jurisdictionMocks = mocksOfType<IJurisdiction[]>()({
numUnique: 30,
numSamplesAudited: 31,
numSamples: 31,
numDiscrepancies: 1,
},
},
],
Expand All @@ -733,7 +731,6 @@ export const jurisdictionMocks = mocksOfType<IJurisdiction[]>()({
numUnique: 10,
numSamplesAudited: 11,
numSamples: 11,
numDiscrepancies: 0,
},
},
{
Expand All @@ -748,7 +745,6 @@ export const jurisdictionMocks = mocksOfType<IJurisdiction[]>()({
numUnique: 20,
numSamplesAudited: 22,
numSamples: 22,
numDiscrepancies: 2,
},
},
{
Expand All @@ -763,7 +759,6 @@ export const jurisdictionMocks = mocksOfType<IJurisdiction[]>()({
numUnique: 30,
numSamplesAudited: 31,
numSamples: 31,
numDiscrepancies: 1,
},
},
],
Expand Down Expand Up @@ -833,7 +828,6 @@ export const jurisdictionMocks = mocksOfType<IJurisdiction[]>()({
numUnique: 10,
numSamplesAudited: 0,
numSamples: 11,
numDiscrepancies: null,
},
},
{
Expand All @@ -848,7 +842,6 @@ export const jurisdictionMocks = mocksOfType<IJurisdiction[]>()({
numUnique: 20,
numSamplesAudited: 0,
numSamples: 22,
numDiscrepancies: null,
},
},
{
Expand All @@ -863,7 +856,6 @@ export const jurisdictionMocks = mocksOfType<IJurisdiction[]>()({
numUnique: 30,
numSamplesAudited: 31,
numSamples: 31,
numDiscrepancies: 0,
},
},
],
Expand Down Expand Up @@ -2015,6 +2007,10 @@ export const aaApiCalls = {
url: '/us-states-counties.json',
response: mapTopology,
},
getDiscrepancyCounts: (response: DiscrepancyCountsByJurisdiction) => ({
url: '/api/election/1/discrepancy-counts',
response,
}),
reopenAuditBoard: {
url:
'/api/election/1/jurisdiction/jurisdiction-id-1/round/round-1/audit-board/audit-board-1/sign-off',
Expand Down
23 changes: 22 additions & 1 deletion client/src/components/useJurisdictions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export interface IJurisdiction {
numUnique: number
numUniqueAudited: number
numBatchesAudited?: number
numDiscrepancies?: number | null
} | null
}

Expand Down Expand Up @@ -144,3 +143,25 @@ export const useJurisdictions = (
return response.jurisdictions
})
}

// { jurisidictionId: discrepancyCount }
export type DiscrepancyCountsByJurisdiction = Record<string, number>

const discrepancyCountsQueryKey = (electionId: string): string[] =>
jurisdictionsQueryKey(electionId).concat('discrepancy-counts')

export const useDiscrepancyCountsByJurisdiction = (
electionId: string,
options: { enabled?: boolean }
): UseQueryResult<DiscrepancyCountsByJurisdiction, ApiError> => {
return useQuery(
discrepancyCountsQueryKey(electionId),
async () => {
const response: DiscrepancyCountsByJurisdiction = await fetchApi(
`/api/election/${electionId}/discrepancy-counts`
)
return response
},
options
)
}
Loading

0 comments on commit f4abffb

Please sign in to comment.