Merge branch 'main' into dependabot/npm_and_yarn/elliptic-6.5.3

This commit is contained in:
rc carter 2021-01-11 11:49:06 -08:00 коммит произвёл GitHub
Родитель 13e98a0118 8d9f3e1abe
Коммит 9a947ee2be
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 293 добавлений и 226 удалений

Просмотреть файл

@ -1,21 +1,20 @@
import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import { Switch, Route } from 'react-router-dom';
import 'normalize.css';
import Layout from './components/Layout';
import NotFoundPage from './pages/NotFoundPage';
import ElectionPage from './pages/ElectionPage';
import HomePage from './pages/HomePage';
import './App.css';
import ElectionPage from './pages/ElectionPage';
function App() {
return (
<Layout>
<Switch>
<Route path="/" exact>
<Redirect to="/election" />
</Route>
<Route path="/election" component={ElectionPage} />
<Route path="/" exact component={HomePage} />
<Route path="/:electionId" component={ElectionPage} />
<Route component={NotFoundPage} />
</Switch>
</Layout>

Просмотреть файл

@ -0,0 +1,23 @@
import React from 'react';
import { Text } from '@fluentui/react';
import moment from 'moment';
import { useTheme } from '@fluentui/react-theme-provider';
// TODO #?? Resolve internalization of dates using i18n
const dateFormat = 'MM/DD/YYYY h:mm a';
export interface ElectionPlaceholderMessageProps {
endDate: string;
}
const ElectionPlaceholderMessage: React.FunctionComponent<ElectionPlaceholderMessageProps> = ({ endDate }) => {
const theme = useTheme();
return (
<Text variant="large" styles={{ root: { color: theme.palette.neutralSecondary } }}>
The election is not finished at this time. Please come back to check for results after
{' ' + moment(endDate).format(dateFormat)}.
</Text>
);
};
export default ElectionPlaceholderMessage;

Просмотреть файл

@ -4,12 +4,8 @@ import { Story, Meta } from '@storybook/react/types-6-0';
import ElectionResults from './ElectionResults';
import electionDescription from '../mocks/description.json';
import tally from '../mocks/tally.json';
import { transformTallyResults } from '../models/electionguard';
import { ElectionDescription } from '../models/election';
import { getDummyQueryResult, queryArgTypes, QueryStoryArgs } from '../util/queryStory';
const electionResults = transformTallyResults('fake-election', tally);
import { Election } from '../models/election';
import { queryArgTypes, QueryStoryArgs } from '../util/queryStory';
export default {
title: 'Components/ElectionResults',
@ -22,22 +18,14 @@ export default {
interface StoryArgs extends QueryStoryArgs {}
const Template: Story<StoryArgs> = ({ queryState }) => {
return (
<ElectionResults
election={electionDescription as ElectionDescription}
electionResultsQuery={getDummyQueryResult(queryState, electionResults)}
/>
);
var election = {
id: electionDescription.election_scope_id,
election_description: electionDescription,
state: 'Published',
} as Election;
return <ElectionResults election={election} />;
};
export const Success = Template.bind({});
Success.storyName = 'Election Results loaded';
Success.args = { queryState: 'success' };
export const Loading = Template.bind({});
Loading.storyName = 'Election Results loading';
Loading.args = { queryState: 'loading' };
export const Error = Template.bind({});
Error.storyName = 'Election Results failed';
Error.args = { queryState: 'error' };
Success.args = {};

Просмотреть файл

@ -1,6 +1,6 @@
import React from 'react';
import { QueryResult } from '../data/queries';
import { ElectionDescription } from '../models/election';
import { useElectionResults } from '../data/queries';
import { Election } from '../models/election';
import { ElectionResultsSummary } from '../models/tally';
import AsyncContent from './AsyncContent';
import ContestResults from './ContestResults';
@ -9,14 +9,15 @@ import LargeCard from './LargeCard';
const errorMessage = 'Unable to retrieve election results at this time.';
export interface ElectionResultsProps {
election: ElectionDescription;
electionResultsQuery: QueryResult<ElectionResultsSummary>;
election: Election;
}
/**
* Render the results of the election
*/
const ElectionResults: React.FunctionComponent<ElectionResultsProps> = ({ election, electionResultsQuery }) => {
const ElectionResults: React.FunctionComponent<ElectionResultsProps> = ({ election }) => {
const electionResultsQuery = useElectionResults(election.id);
return (
<AsyncContent query={electionResultsQuery} errorMessage={errorMessage}>
{(results) => {
@ -28,7 +29,7 @@ const ElectionResults: React.FunctionComponent<ElectionResultsProps> = ({ electi
<ContestResults
results={contest.results}
contest={contest.description!}
candidates={election.candidates}
candidates={election.election_description.candidates}
/>
</LargeCard>
))}
@ -39,12 +40,12 @@ const ElectionResults: React.FunctionComponent<ElectionResultsProps> = ({ electi
);
};
function getContests(election: ElectionDescription, results: ElectionResultsSummary) {
function getContests(election: Election, results: ElectionResultsSummary) {
const contests = Object.entries(results.election_results)
.map(([contestId, contestResults]) => ({
id: contestId,
results: contestResults,
description: election.contests.find((c) => c.object_id === contestId),
description: election.election_description.contests.find((c) => c.object_id === contestId),
}))
.filter((c) => !!c.description);
return contests;

Просмотреть файл

@ -7,7 +7,7 @@ import { useTheme } from '@fluentui/react-theme-provider';
// TODO #?? Resolve internalization of language using i18n
const defaultElectionName = 'Election';
// TODO #?? Resolve internalization of dates using i18n
const dateFormat = 'MM/DD/YYYY';
const dateFormat = 'MM/DD/YYYY h:mm a';
export interface ElectionTitleProps {
electionName?: string;
@ -21,7 +21,7 @@ const ElectionTitle: React.FunctionComponent<ElectionTitleProps> = ({ electionNa
<Title title={electionName ?? defaultElectionName}>
<Text as="span" styles={{ root: { color: theme.palette.neutralSecondary } }}>{`${moment(startDate).format(
dateFormat
)}-${moment(endDate).format(dateFormat)}`}</Text>
)} - ${moment(endDate).format(dateFormat)}`}</Text>
</Title>
);
};

Просмотреть файл

@ -1,6 +1,5 @@
import React from 'react';
import { useTheme } from '@fluentui/react-theme-provider';
import { Card, CardSection } from '@uifabric/react-cards';
export interface LargeCardProps {
alignToStart?: boolean;
@ -9,20 +8,23 @@ export interface LargeCardProps {
const LargeCard: React.FunctionComponent<LargeCardProps> = ({ alignToStart, children }) => {
const theme = useTheme();
return (
<Card
styles={{
root: {
padding: theme.spacing.l1,
backgroundColor: theme.palette.white,
marginBottom: theme.spacing.l1,
alignItems: alignToStart ? 'flex-start' : 'stretch',
height: 'auto',
maxWidth: 'auto',
},
<div
style={{
padding: theme.spacing.l1,
backgroundColor: theme.palette.white,
marginBottom: theme.spacing.l1,
alignItems: alignToStart ? 'flex-start' : 'stretch',
height: 'auto',
maxWidth: 'auto',
borderTopLeftRadius: 2,
borderTopRightRadius: 2,
borderBottomRightRadius: 2,
borderBottomLeftRadius: 2,
boxShadow: 'rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px',
}}
>
<CardSection styles={{ root: { width: '100%' } }}>{children || null}</CardSection>
</Card>
{children || null}
</div>
);
};

Просмотреть файл

@ -10,7 +10,7 @@ export default {
component: TrackerDialog,
} as Meta;
const Template: Story<TrackerDialogProps> = ({ tracker, confirmed, isLoading }) => {
const Template: Story<TrackerDialogProps> = ({ tracker, trackerState }) => {
initializeIcons();
const [hidden, { toggle: toggleHideDialog }] = useBoolean(true);
@ -18,13 +18,7 @@ const Template: Story<TrackerDialogProps> = ({ tracker, confirmed, isLoading })
<>
<DefaultButton secondaryText="Opens the Sample Dialog" onClick={toggleHideDialog} text="Open Dialog" />
<TrackerDialog
hidden={hidden}
isLoading={isLoading}
tracker={tracker}
onDismiss={toggleHideDialog}
confirmed={confirmed}
/>
<TrackerDialog hidden={hidden} tracker={tracker} onDismiss={toggleHideDialog} trackerState={trackerState} />
</>
);
};
@ -32,32 +26,35 @@ const Template: Story<TrackerDialogProps> = ({ tracker, confirmed, isLoading })
export const SuccessTrackerDialog = Template.bind({});
SuccessTrackerDialog.storyName = 'Confirmed Tracker Dialog';
SuccessTrackerDialog.args = {
isLoading: false,
tracker: 'confirmed-confirmed-confirmed-confirmed-confirmed',
confirmed: true,
trackerState: 'confirmed',
};
export const SpoiledTrackerDialog = Template.bind({});
SpoiledTrackerDialog.storyName = 'Spoiled Tracker Dialog';
SpoiledTrackerDialog.args = {
tracker: 'spoiled-spoiled-spoiled-spoiled-spoiled',
trackerState: 'spoiled',
};
export const UnknownTrackerDialog = Template.bind({});
UnknownTrackerDialog.storyName = 'Unknown Tracker Dialog';
UnknownTrackerDialog.args = {
isLoading: false,
tracker: 'unknown-unknown-unknown-unknown-unknown-unknown',
confirmed: false,
trackerState: 'unknown',
};
export const LongTrackerDialog = Template.bind({});
LongTrackerDialog.storyName = 'Long Tracker Dialog';
LongTrackerDialog.args = {
isLoading: false,
tracker:
'long-long-long-long-long-long-long-long-long-long-long-long-long-long-long-long-long-long-long-long-long-long-long',
confirmed: true,
trackerState: 'confirmed',
};
export const LoadingTrackerDialog = Template.bind({});
LoadingTrackerDialog.storyName = 'Loading Dialog';
LoadingTrackerDialog.args = {
isLoading: true,
tracker: '',
confirmed: false,
trackerState: 'loading',
};

Просмотреть файл

@ -1,74 +1,44 @@
import React from 'react';
import { Dialog, DialogType, IDialogContentProps } from 'office-ui-fabric-react/lib/Dialog';
import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog';
import { useTheme } from '@fluentui/react-theme-provider';
import StatusMessage from './StatusMessage';
import { Spinner, SpinnerSize } from '@fluentui/react';
import { IPalette, Spinner, SpinnerSize } from '@fluentui/react';
// Text for Internationalization
const confirmedTrackerTitle = 'Ballot Confirmed';
const confirmedTrackerMessage =
'The following ballot was securely submitted and counted as part of the election tally.';
const loadingTrackerTitle = 'Searching';
const loadingTrackerMessage = '';
const unknownTrackerTitle = 'Ballot Not Found';
const unknownTrackerMessage = `
There was no record found of the following ballot and it is not part of the official tally.
Please double check the spelling of the tracker and search again.
`;
export type TrackerDialogStateOption = 'loading' | 'spoiled' | 'confirmed' | 'unknown';
// Icons
const confirmedIcon = 'Completed';
const loadingIcon = 'Search';
const unknownIcon = 'Unknown';
interface TrackerCodeProps {
tracker: string;
confirmed: boolean;
interface TrackerDialogStateSettings {
icon: string;
color: keyof IPalette;
title: string;
message: string;
}
const TrackerCode: React.FunctionComponent<TrackerCodeProps> = ({ tracker, confirmed }) => {
const theme = useTheme();
return (
<div
style={{
display: 'flex',
borderColor: confirmed ? theme.palette.green : theme.palette.themePrimary,
borderStyle: 'solid',
borderWidth: 2,
padding: 8,
}}
>
{tracker}
</div>
);
};
const dialogStyles = { main: { maxWidth: 450 } };
const confirmedDialogContentProps: IDialogContentProps = {
type: DialogType.largeHeader,
title: <StatusMessage icon={confirmedIcon} colorName="green" message={confirmedTrackerTitle} />,
subText: confirmedTrackerMessage,
styles: ({ theme }) => ({
content: { borderColor: theme.palette.green },
}),
};
const unknownDialogContentProps: IDialogContentProps = {
type: DialogType.largeHeader,
title: <StatusMessage icon={unknownIcon} colorName="themePrimary" message={unknownTrackerTitle} />,
subText: unknownTrackerMessage,
styles: ({ theme }) => ({
content: { borderColor: theme.palette.themePrimary },
}),
};
const loadingDialogContentProps: IDialogContentProps = {
type: DialogType.largeHeader,
title: <StatusMessage icon={loadingIcon} colorName="neutralPrimary" message={loadingTrackerTitle} />,
subText: loadingTrackerMessage,
styles: ({ theme }) => ({
content: { borderColor: theme.palette.neutralLight },
}),
const states: { [state in TrackerDialogStateOption]: TrackerDialogStateSettings } = {
loading: {
icon: 'Search',
color: 'neutralPrimary',
title: 'Searching',
message: '',
},
confirmed: {
icon: 'Completed',
color: 'green',
title: 'Ballot Confirmed',
message: 'The following ballot was securely submitted and counted as part of the election tally.',
},
spoiled: {
icon: 'Error',
color: 'redDark',
title: 'Ballot Spoiled',
message: 'The following ballot was not included in the election tally.',
},
unknown: {
icon: 'Unknown',
color: 'themePrimary',
title: 'Ballot Not Found',
message: `There was no record found of the following ballot and it is not part of the official tally.
Please double check the spelling of the tracker and search again.`,
},
};
/**
@ -83,10 +53,8 @@ export interface TrackerDialogProps {
onDismissed?: () => void;
/** Tracker code */
tracker: string;
/** Confirmation status of tracker */
confirmed: boolean;
/** The application is looking for the tracker */
isLoading?: boolean;
/** The visual state of the dialog */
trackerState: TrackerDialogStateOption;
}
/**
@ -94,11 +62,10 @@ export interface TrackerDialogProps {
*/
const TrackerDialog: React.FunctionComponent<TrackerDialogProps> = ({
hidden,
isLoading = false,
onDismiss,
onDismissed,
tracker,
confirmed,
trackerState,
}) => {
const modalProps = React.useMemo(
() => ({
@ -109,17 +76,50 @@ const TrackerDialog: React.FunctionComponent<TrackerDialogProps> = ({
[onDismissed]
);
const dialogContentProps = isLoading
? loadingDialogContentProps
: confirmed
? confirmedDialogContentProps
: unknownDialogContentProps;
const { color, icon, message, title } = states[trackerState];
const isLoading = trackerState === 'loading';
return (
<Dialog hidden={hidden} onDismiss={onDismiss} dialogContentProps={dialogContentProps} modalProps={modalProps}>
{isLoading ? <Spinner size={SpinnerSize.large} /> : <TrackerCode tracker={tracker} confirmed={confirmed} />}
<Dialog
hidden={hidden}
onDismiss={onDismiss}
dialogContentProps={{
type: DialogType.largeHeader,
title: <StatusMessage icon={icon} colorName={color} message={title} />,
subText: message,
styles: ({ theme }) => ({
content: { borderColor: theme.palette[color] },
}),
}}
modalProps={modalProps}
>
{isLoading ? <Spinner size={SpinnerSize.large} /> : <TrackerCode tracker={tracker} color={color} />}
</Dialog>
);
};
interface TrackerCodeProps {
tracker: string;
color: keyof IPalette;
}
const TrackerCode: React.FunctionComponent<TrackerCodeProps> = ({ tracker, color }) => {
const theme = useTheme();
return (
<div
style={{
display: 'flex',
borderColor: theme.palette[color],
borderStyle: 'solid',
borderWidth: 2,
padding: 8,
}}
>
{tracker}
</div>
);
};
const dialogStyles = { main: { maxWidth: 450 } };
export default TrackerDialog;

Просмотреть файл

@ -4,7 +4,7 @@ import { useSearchBallots } from '../data/queries';
import { TrackedBallot } from '../models/tracking';
const DEFAULT_MINIMUM_QUERY_LENGTH = 3;
const DEFAULT_DEBOUNCE_TIME_MS = 300;
const DEFAULT_DEBOUNCE_TIME_MS = 250;
export interface SearchOptions {
minimumQueryLength?: number;
@ -47,7 +47,7 @@ export function useSearch(electionId: string, options: SearchOptions = {}): Sear
// If the query is not valid, the search results will be undefined and the state of the fetch
// will be "idle".
const { data: searchResults, isIdle, isLoading } = useSearchBallots(electionId, preparedQuery, isValidQuery);
if (searchResults) {
if (searchResults !== undefined) {
latestQuery.current = query;
latestResults.current = searchResults;
}
@ -62,7 +62,7 @@ export function useSearch(electionId: string, options: SearchOptions = {}): Sear
}, [search]);
return {
results: searchResults || latestResults.current,
results: isValidQuery ? searchResults || latestResults.current : [],
isLoading: isIdle || isLoading,
search,
clear,

Просмотреть файл

@ -2,13 +2,16 @@ import React, { useEffect, useState } from 'react';
import { Label } from '@fluentui/react';
import { Theme, useTheme } from '@fluentui/react-theme-provider';
import Autosuggest, { InputProps } from 'react-autosuggest';
import { Route, Switch, useHistory, useRouteMatch } from 'react-router-dom';
import { Route, Switch, useHistory, useParams, useRouteMatch } from 'react-router-dom';
import take from 'lodash/take';
import LargeCard from './LargeCard';
import { TrackedBallot } from '../models/tracking';
import TrackerDialog from './TrackerDialog';
import TrackerDialog, { TrackerDialogStateOption } from './TrackerDialog';
import { useSearch } from './TrackerSearch.hooks';
const MAX_RESULTS_TO_SHOW = 5;
export interface TrackerSearchProps {
electionId: string;
}
@ -70,13 +73,13 @@ const fromTheme = (theme: Theme) => {
const TrackerSearch: React.FunctionComponent<TrackerSearchProps> = ({ electionId }) => {
const history = useHistory();
const { path, url } = useRouteMatch();
const theme = useTheme();
// Track the raw input value from the user
const [inputValue, setInputValue] = useState<string>('');
const [selectedBallot, setSelectedBallot] = useState<TrackedBallot | undefined>();
const { results, isLoading, search, clear } = useSearch(electionId);
const { results, search, clear } = useSearch(electionId);
// Wire up the input element to the search input value
const inputProps: InputProps<TrackedBallot> = {
@ -93,7 +96,7 @@ const TrackerSearch: React.FunctionComponent<TrackerSearchProps> = ({ electionId
<Label>Ballot Search</Label>
<Autosuggest
theme={fromTheme(theme)}
suggestions={results}
suggestions={take(results, MAX_RESULTS_TO_SHOW)}
onSuggestionsFetchRequested={({ value }) => {
search(value);
}}
@ -101,7 +104,8 @@ const TrackerSearch: React.FunctionComponent<TrackerSearchProps> = ({ electionId
setInputValue('');
clear();
}}
onSuggestionSelected={(event, { suggestion }) => {
onSuggestionSelected={(_event, { suggestion }) => {
setSelectedBallot(suggestion);
const tracker = suggestion.tracker_words;
history.push(`${url}/track/${tracker}`);
}}
@ -112,16 +116,7 @@ const TrackerSearch: React.FunctionComponent<TrackerSearchProps> = ({ electionId
<Switch>
<Route
path={`${path}/track/:tracker`}
render={() => (
<TrackerResults
searchResults={results}
updateQuery={(newQuery) => {
setInputValue(newQuery);
search(newQuery);
}}
isQuerying={isLoading}
/>
)}
render={() => <TrackerResults parentPath={url} selectedBallot={selectedBallot} />}
/>
</Switch>
</LargeCard>
@ -130,35 +125,54 @@ const TrackerSearch: React.FunctionComponent<TrackerSearchProps> = ({ electionId
};
interface TrackerResultsProps {
isQuerying: boolean;
searchResults: TrackedBallot[];
updateQuery: (query: string) => void;
parentPath: string;
selectedBallot: TrackedBallot | undefined;
}
const TrackerResults: React.FunctionComponent<TrackerResultsProps> = ({ searchResults, isQuerying, updateQuery }) => {
/**
* Displays a dialog of search results.
*
* If launched from a search, the selected ballot is provided directly.
* Otherwise, it will need to trigger a search for the ballot.
*/
const TrackerResults: React.FunctionComponent<TrackerResultsProps> = ({ selectedBallot, parentPath }) => {
const history = useHistory();
const { params } = useRouteMatch<{ tracker: string }>();
const tracker = params.tracker;
const { electionId, tracker } = useParams<{ electionId: string; tracker: string }>();
const isMatch = (ballot: TrackedBallot) => ballot.tracker_words === tracker;
const existingBallot = searchResults?.find(isMatch);
const isLoading = !existingBallot && isQuerying;
const { results, search, isLoading } = useSearch(electionId, { debounceTimeInMs: 0 });
useEffect(() => {
if (!existingBallot) {
updateQuery(tracker);
// If navigating directly to this route, we only have a tracker and must search for it
if (!selectedBallot) {
search(tracker);
}
}, [tracker, existingBallot, updateQuery]);
}, [search, selectedBallot, tracker]);
// Check for a ballot to display
let existingBallot: TrackedBallot | undefined;
if (selectedBallot) {
existingBallot = selectedBallot;
} else {
existingBallot = results.find((ballot) => ballot.tracker_words === tracker);
}
const showLoading = !existingBallot && isLoading;
let trackerState: TrackerDialogStateOption;
if (showLoading) {
trackerState = 'loading';
} else if (existingBallot) {
trackerState = existingBallot.state === 'Cast' ? 'confirmed' : 'spoiled';
} else {
trackerState = 'unknown';
}
return (
<TrackerDialog
hidden={false}
isLoading={isLoading}
tracker={tracker}
confirmed={!!existingBallot}
trackerState={trackerState}
onDismiss={() => {
history.replace('/');
history.replace(parentPath);
}}
/>
);

Просмотреть файл

@ -1,4 +1,4 @@
import { ElectionDescription } from '../models/election';
import { Election } from '../models/election';
import { ElectionResultsSummary } from '../models/tally';
import { TrackedBallot } from '../models/tracking';
@ -6,7 +6,7 @@ import { TrackedBallot } from '../models/tracking';
* Provides access to election data and search functionality.
*/
export interface DataAccess {
getElectionDescription(): Promise<ElectionDescription>;
getElections(): Promise<Election[]>;
getElectionResults(electionId: string): Promise<ElectionResultsSummary>;
searchBallots(electionId: string, query: string): Promise<TrackedBallot[]>;
}

Просмотреть файл

@ -1,5 +1,5 @@
import path from 'path';
import { ElectionDescription } from '../models/election';
import { Election, ElectionDescription } from '../models/election';
import {
CiphertextAcceptedBallot,
PlaintextTally,
@ -14,8 +14,14 @@ import { DataAccess } from './DataAccess';
* DataAccess implementation for static published ElectionGuard data.
*/
export class PublishedDataAccess implements DataAccess {
getElectionDescription(): Promise<ElectionDescription> {
return loadPublishedFile('description.json');
async getElections(): Promise<Election[]> {
const electionDescription = await loadPublishedFile<ElectionDescription>('description.json');
const election: Election = {
id: electionDescription.election_scope_id,
election_description: electionDescription,
state: 'Published',
};
return [election];
}
async getElectionResults(electionId: string): Promise<ElectionResultsSummary> {

Просмотреть файл

@ -1,11 +1,11 @@
import { useQuery } from 'react-query';
import { ElectionDescription } from '../models/election';
import { Election } from '../models/election';
import { ElectionResultsSummary } from '../models/tally';
import { TrackedBallot } from '../models/tracking';
import { useDataAccess } from './DataAccessProvider';
export const QUERIES = {
ELECTION_DESCRIPTION: 'ELECTION_DESCRIPTION',
ELECTIONS: 'ELECTIONS',
ELECTION_RESULTS: 'ELECTION_RESULTS',
SEARCH_BALLOTS: 'SEARCH_BALLOTS',
};
@ -18,12 +18,12 @@ export interface QueryResult<T> {
}
/**
* Fetch the election description
* Fetch the available elections
* @param condition An optional boolean value which, if false, will prevent the query from running.
*/
export function useElectionDescription(condition: boolean = true): QueryResult<ElectionDescription> {
export function useElections(condition: boolean = true): QueryResult<Election[]> {
const dataAccess = useDataAccess();
return useQuery(QUERIES.ELECTION_DESCRIPTION, () => dataAccess.getElectionDescription(), { enabled: condition });
return useQuery(QUERIES.ELECTIONS, () => dataAccess.getElections(), { enabled: condition });
}
/**
@ -34,7 +34,7 @@ export function useElectionDescription(condition: boolean = true): QueryResult<E
*/
export function useElectionResults(electionId: string, condition: boolean = true): QueryResult<ElectionResultsSummary> {
const dataAccess = useDataAccess();
return useQuery([QUERIES.ELECTION_DESCRIPTION, electionId], () => dataAccess.getElectionResults(electionId), {
return useQuery([QUERIES.ELECTION_RESULTS, electionId], () => dataAccess.getElectionResults(electionId), {
enabled: condition && electionId,
});
}
@ -53,7 +53,7 @@ export function useSearchBallots(
): QueryResult<TrackedBallot[]> {
const dataAccess = useDataAccess();
return useQuery(
[QUERIES.ELECTION_DESCRIPTION, electionId, trackerQuery],
[QUERIES.SEARCH_BALLOTS, electionId, trackerQuery],
() => dataAccess.searchBallots(electionId, trackerQuery),
{
// The search will stay in 'idle' state until this is satisfied

Просмотреть файл

@ -4,7 +4,7 @@ import mockDescription from './description.json';
import mockTally from './tally.json';
import mockBallots from './ballots.json';
import { CiphertextAcceptedBallot, transformBallotForTracking, transformTallyResults } from '../models/electionguard';
import { ElectionDescription } from '../models/election';
import { Election } from '../models/election';
import { ElectionResultsSummary } from '../models/tally';
import { TrackedBallot } from '../models/tracking';
@ -17,8 +17,13 @@ const trackedBallots = (mockBallots as CiphertextAcceptedBallot[]).map((ballot)
* DataAccess implementation for in-memory synchronous mocked data
*/
export class MockDataAccess implements DataAccess {
async getElectionDescription(): Promise<ElectionDescription> {
return mockDescription as any;
async getElections(): Promise<Election[]> {
const mockElection: Election = {
id: mockDescription.election_scope_id,
election_description: mockDescription as any,
state: 'Published',
};
return [mockElection];
}
async getElectionResults(electionId: string): Promise<ElectionResultsSummary> {

Просмотреть файл

@ -1,5 +1,13 @@
import { InternationalizedText } from './internationalizedText';
export type ElectionState = 'New' | 'Open' | 'Closed' | 'Published';
export interface Election {
id: string;
election_description: ElectionDescription;
state: ElectionState;
}
export interface ElectionDescription {
election_scope_id: string;
start_date: string;

Просмотреть файл

@ -1,14 +0,0 @@
import React from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import ElectionPage, { ElectionPageProps } from './ElectionPage';
export default {
title: 'Pages/ElectionPage',
component: ElectionPage,
} as Meta;
const Template: Story<ElectionPageProps> = (props) => <ElectionPage {...props} />;
export const Page = Template.bind({});
Page.storyName = 'Render Election';
Page.args = {};

Просмотреть файл

@ -2,37 +2,50 @@ import React from 'react';
import { Stack } from '@fluentui/react';
import AsyncContent from '../components/AsyncContent';
import ElectionResults from '../components/ElectionResults';
import ElectionHeader from '../components/ElectionTitle';
import ElectionTitle from '../components/ElectionTitle';
import ElectionPlaceholderMessage from '../components/ElectionPlaceholderMessage';
import TrackerSearch from '../components/TrackerSearch';
import { useElectionDescription, useElectionResults } from '../data/queries';
import ElectionResults from '../components/ElectionResults';
import { useElections } from '../data/queries';
import { useLocalization } from '../localization/LocalizationProvider';
import { useParams } from 'react-router-dom';
import Title from '../components/Title';
export interface ElectionPageProps {}
const ElectionPage: React.FunctionComponent<ElectionPageProps> = () => {
const { electionId } = useParams<{ electionId: string }>();
const { translate } = useLocalization();
const electionQuery = useElectionDescription();
const electionId = electionQuery.data?.election_scope_id || '';
const electionResultsQuery = useElectionResults(electionId);
const electionsQuery = useElections();
return (
<Stack>
<AsyncContent query={electionQuery} errorMessage="Unable to load the election at this time.">
{(election) => (
<>
<ElectionHeader
electionName={translate(election.name)}
startDate={election.start_date}
endDate={election.end_date}
/>
<AsyncContent query={electionsQuery} errorMessage="Unable to load the election at this time.">
{(elections) => {
const election = elections.find((e) => e.id === electionId);
<TrackerSearch electionId={election.election_scope_id} />
if (!election) {
return <Title title="We're having trouble finding this election. Please try again." />;
}
<ElectionResults election={election} electionResultsQuery={electionResultsQuery} />
</>
)}
return (
<>
<ElectionTitle
electionName={translate(election!.election_description.name)}
startDate={election!.election_description.start_date}
endDate={election!.election_description.end_date}
/>
{election.state === 'Published' ? (
<>
<TrackerSearch electionId={election!.id} />
<ElectionResults election={election!} />
</>
) : (
<ElectionPlaceholderMessage endDate={election!.election_description.end_date} />
)}
</>
);
}}
</AsyncContent>
</Stack>
);

25
src/pages/HomePage.tsx Normal file
Просмотреть файл

@ -0,0 +1,25 @@
import React from 'react';
import { Stack } from '@fluentui/react';
import AsyncContent from '../components/AsyncContent';
import { useElections } from '../data/queries';
import { Redirect } from 'react-router-dom';
export interface HomePageProps {}
const ElectionPage: React.FunctionComponent<HomePageProps> = () => {
const electionsQuery = useElections();
return (
<Stack>
<AsyncContent query={electionsQuery} errorMessage="Unable to load any elections at this time.">
{(elections) => {
const election = elections[0];
return <Redirect to={`/${election.id}`} />;
}}
</AsyncContent>
</Stack>
);
};
export default ElectionPage;