* Add spinner, refactor names

* Error handling

* Set manifest_hash

* Refactor out setManifestHash

* Fix infinite loop in joint key select step

* Set number_of_guardians and quorum

* Fix VS Code debugging profile

* Fix bug with manifest hash not being sent

* Don't display error if there is no error on joint key upload step

* Create a hooks folder and move existing hook into it

* Converted client retrieval to hooks per PR feedback

* Better UI for loading icon

* Final tweaks

Disable buttons while loading
Redirect to election page
Remove caption
Fix warnings

* PR feedback
This commit is contained in:
Lee Richardson 2022-02-09 11:11:28 -05:00 коммит произвёл GitHub
Родитель ac5e56a025
Коммит b5803475f5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 165 добавлений и 84 удалений

12
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3001",
"webRoot": "${workspaceFolder}"
}
]
}

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

@ -3,7 +3,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
import { ThemeProvider, Theme, StyledEngineProvider } from '@mui/material';
import { AuthenticatedLayout } from './layouts';
import { LoginPage } from './pages';
import useToken from './useToken';
import useToken from './hooks/useToken';
import AuthenticatedRoutes from './routes/AuthenticatedRoutes';
import UnauthenticatedLayout from './layouts/UnauthenticatedLayout';

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

@ -4,7 +4,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { MessageId } from '../../lang';
import useToken from '../../useToken';
import useToken from '../../hooks/useToken';
import { ReactComponent as ElectionGuardLogo } from '../../images/electionguard-logo.svg';
import routeIds from '../../routes/RouteIds';

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

@ -1,12 +1,12 @@
import {
ApiClientFactory,
ClientFactory,
SubmitElectionRequest,
ValidateManifestRequest,
} from '@electionguard/api-client';
import { Box } from '@mui/material';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useV1Client } from '../../hooks/useClient';
import routeIds from '../../routes/RouteIds';
import { createEnumStepper } from '../../utils/EnumStepper';
@ -43,19 +43,34 @@ export const ElectionSetupWizard: React.FC = () => {
setStep(nextStep);
};
const handleChanged = (requestFromStep: SubmitElectionRequest) => {
const newRequest = {
const newRequest: SubmitElectionRequest = {
...request,
...requestFromStep,
context: {
...request.context,
...requestFromStep.context,
},
};
setRequest(newRequest);
};
const handleUploadManifest = (manifestJson: ValidateManifestRequest) => {
setManifest(manifestJson);
};
const setManifestHash = (manifestHash: string): SubmitElectionRequest => {
const requestWithManifestHash = {
...request,
context: { ...request.context, manifest_hash: manifestHash },
};
setRequest(requestWithManifestHash);
return requestWithManifestHash;
};
const v1Client = useV1Client();
const handleSubmit = async () => {
const v1Client = ClientFactory.GetV1Client();
await v1Client.manifestPut(manifest);
// todo: submit data to API
const manifestCreationResult = await v1Client.manifestPut(manifest);
const requestWithManifestHash = setManifestHash(manifestCreationResult.manifest_hash);
await v1Client.electionPut(requestWithManifestHash);
setStep(ElectionSetupStep.SetupComplete);
};
@ -77,14 +92,14 @@ export const ElectionSetupWizard: React.FC = () => {
{step === ElectionSetupStep.ManifestPreview && (
<WizardStep active={step === ElectionSetupStep.ManifestPreview}>
<ManifestPreviewStep
onNext={handleSubmit}
backToMenu={() => setStep(ElectionSetupStep.ManifestUpload)}
onSubmit={handleSubmit}
onCancel={() => setStep(ElectionSetupStep.ManifestUpload)}
preview={service.getManifestPreview(manifest, request)}
/>
</WizardStep>
)}
<WizardStep active={step === ElectionSetupStep.SetupComplete}>
<SetupCompleteStep onComplete={() => navigate(routeIds.home)} />
<SetupCompleteStep onComplete={() => navigate(routeIds.electionList)} />
</WizardStep>
</Box>
);

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

@ -1,4 +1,4 @@
import { ClientFactory, KeyCeremony, SubmitElectionRequest } from '@electionguard/api-client';
import { KeyCeremony, SubmitElectionRequest } from '@electionguard/api-client';
import {
Box,
Button,
@ -18,6 +18,7 @@ import { QueryClient, QueryClientProvider } from 'react-query';
import { Message, MessageId } from '../../../lang';
import IconHeader from '../../IconHeader';
import { useCeremonyClient } from '../../../hooks/useClient';
const useStyles = makeStyles((theme) => ({
root: {
@ -59,25 +60,29 @@ const JointKeySelectStep: React.FC<JointKeySelectStepProps> = ({ onNext, onChang
}
};
const ceremonyClient = ClientFactory.GetCeremonyClient();
const ceremonyClient = useCeremonyClient();
useEffect(() => {
const findKeyCeremonies = async () => {
await ceremonyClient.find(0, 100, { filter: {} }).then((response) => {
setKeyCeremonies(response.key_ceremonies);
});
};
const getKeyCeremonies = async () => {
await ceremonyClient.find(0, 100, { filter: {} }).then((response) => {
setKeyCeremonies(response.key_ceremonies);
});
};
findKeyCeremonies();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const queryClient = new QueryClient();
const classes = useStyles();
useEffect(() => {
getKeyCeremonies();
});
const onNextClick = () => {
const submitElectionRequest = {
key_name: keyCeremony?.key_name,
context: {
quorum: keyCeremony?.quorum,
number_of_guardians: keyCeremony?.number_of_guardians,
},
} as SubmitElectionRequest;
onChanged(submitElectionRequest);
onNext();

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

@ -86,7 +86,7 @@ const JointKeyUploadStep: React.FC<JointKeyUploadStepProps> = ({ onNext, onChang
Icon={KeyIcon}
/>
<div className={classes.error}>{error}</div>
{error && <div className={classes.error}>{error}</div>}
<Button
disabled={uploading}

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

@ -1,15 +1,24 @@
import { ManifestPreview } from '@electionguard/api-client';
import { Box, Button, Container, Grid, Table, TableBody, TableCell, TableRow } from '@mui/material';
import {
Box,
Button,
CircularProgress,
Container,
Table,
TableBody,
TableCell,
TableRow,
} from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import React, { useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { Message, MessageId } from '../../../lang';
import IconHeader from '../../IconHeader';
export interface ManifestPreviewStepProps {
onNext: () => void;
backToMenu: () => void;
onSubmit: () => Promise<void>;
onCancel: () => void;
preview: ManifestPreview;
}
@ -18,6 +27,10 @@ const useStyles = makeStyles((theme) => ({
flexGrow: 1,
height: '100%',
},
error: {
color: 'red',
marginBottom: theme.spacing(2),
},
spaced: {
marginBottom: theme.spacing(2),
},
@ -27,24 +40,45 @@ const useStyles = makeStyles((theme) => ({
button: {
marginRight: theme.spacing(2),
},
loading: {
marginLeft: theme.spacing(1),
},
}));
const ManifestPreviewStep: React.FC<ManifestPreviewStepProps> = ({
onNext,
backToMenu,
onSubmit,
onCancel,
preview,
}) => {
const classes = useStyles();
const onButtonClick = () => {
onNext();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const intl = useIntl();
const setIntlError = (id: string) => {
const message = intl.formatMessage({
id,
});
setError(message);
};
const onButtonClick = async () => {
setLoading(true);
try {
await onSubmit();
} catch (ex) {
setIntlError(MessageId.ElectionSetup_ManifestPreview_SubmitError);
} finally {
setLoading(false);
}
};
return (
<Grid container className={classes.root}>
<Container maxWidth="md">
<Box display="flex" flexDirection="column" alignItems="center">
<IconHeader title={new Message(MessageId.ElectionSetupManifestPreviewTitle)} />
<Table aria-label="caption table" className={classes.spaced}>
<Container maxWidth="md">
<Box display="flex" flexDirection="column" alignItems="center">
<IconHeader title={new Message(MessageId.ElectionSetup_ManifestPreview_Title)} />
<Table aria-label="caption table" className={classes.spaced}>
<TableBody>
<TableRow>
<TableCell className={classes.property}>
<FormattedMessage id={MessageId.ElectionSetup_ManifestPreview_Id} />
@ -54,7 +88,7 @@ const ManifestPreviewStep: React.FC<ManifestPreviewStepProps> = ({
<TableRow>
<TableCell className={classes.property}>
<FormattedMessage
id={MessageId.ElectionSetupManifestPreviewPropertyName}
id={MessageId.ElectionSetup_ManifestPreview_PropertyName}
/>
</TableCell>
<TableCell>{preview.name}</TableCell>
@ -63,7 +97,7 @@ const ManifestPreviewStep: React.FC<ManifestPreviewStepProps> = ({
<TableCell className={classes.property}>
<FormattedMessage
id={
MessageId.ElectionSetupManifestPreviewPropertyNumberOfContests
MessageId.ElectionSetup_ManifestPreview_PropertyNumberOfContests
}
/>
</TableCell>
@ -73,7 +107,7 @@ const ManifestPreviewStep: React.FC<ManifestPreviewStepProps> = ({
<TableCell className={classes.property}>
<FormattedMessage
id={
MessageId.ElectionSetupManifestPreviewPropertyNumberOfStyles
MessageId.ElectionSetup_ManifestPreview_PropertyNumberOfStyles
}
/>
</TableCell>
@ -82,7 +116,7 @@ const ManifestPreviewStep: React.FC<ManifestPreviewStepProps> = ({
<TableRow>
<TableCell className={classes.property}>
<FormattedMessage
id={MessageId.ElectionSetupManifestPreviewPropertyStartDate}
id={MessageId.ElectionSetup_ManifestPreview_PropertyStartDate}
/>
</TableCell>
<TableCell>
@ -93,7 +127,7 @@ const ManifestPreviewStep: React.FC<ManifestPreviewStepProps> = ({
<TableRow>
<TableCell className={classes.property}>
<FormattedMessage
id={MessageId.ElectionSetupManifestPreviewPropertyEndDate}
id={MessageId.ElectionSetup_ManifestPreview_PropertyEndDate}
/>
</TableCell>
<TableCell>
@ -101,36 +135,37 @@ const ManifestPreviewStep: React.FC<ManifestPreviewStepProps> = ({
{preview.endDate.toLocaleTimeString()}
</TableCell>
</TableRow>
<caption>
<FormattedMessage
id={MessageId.ElectionSetupManifestPreviewCaption}
defaultMessage="Preview of Manifest"
/>
</caption>
<TableBody />
</Table>
</Box>
<Box display="flex">
<Button
variant="contained"
color="secondary"
onClick={onButtonClick}
className={classes.button}
>
<FormattedMessage
id={MessageId.ElectionSetupManifestPreviewNext}
defaultMessage="Submit"
</TableBody>
</Table>
</Box>
{error && <div className={classes.error}>{error}</div>}
<Box display="flex">
<Button
variant="contained"
color="secondary"
disabled={loading}
onClick={onButtonClick}
className={classes.button}
>
<FormattedMessage id={MessageId.ElectionSetup_ManifestPreview_Next} />
{loading && (
<CircularProgress
size={12}
variant="indeterminate"
className={classes.loading}
/>
</Button>
<Button color="primary" onClick={backToMenu} className={classes.button}>
<FormattedMessage
id={MessageId.ElectionSetupManifestPreviewBackToMenu}
defaultMessage="Cancel"
/>
</Button>
</Box>
</Container>
</Grid>
)}
</Button>
<Button
color="primary"
onClick={onCancel}
className={classes.button}
disabled={loading}
>
<FormattedMessage id={MessageId.ElectionSetup_ManifestPreview_BackToMenu} />
</Button>
</Box>
</Container>
);
};

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

@ -0,0 +1,13 @@
import { AuthClient, CeremonyClient, ClientFactory, V1Client } from '@electionguard/api-client';
export function useV1Client(): V1Client {
return ClientFactory.GetV1Client();
}
export function useCeremonyClient(): CeremonyClient {
return ClientFactory.GetCeremonyClient();
}
export function useAuthClient(): AuthClient {
return ClientFactory.GetAuthClient();
}

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

@ -37,16 +37,16 @@ enum MessageId {
ElectionSetup_JointKeySelect_Next = 'election_setup.joint_key_select.next',
ElectionSetup_ManifestPreview_Id = 'election_setup.manifest_preview.property.id',
ElectionSetupManifestPreviewTitle = 'election_setup.manifest_preview.title',
ElectionSetupManifestPreviewPropertyName = 'election_setup.manifest_preview.property.name',
ElectionSetupManifestPreviewPropertyNumberOfContests = 'election_setup.manifest_preview.property.number_of_contests',
ElectionSetupManifestPreviewPropertyNumberOfStyles = 'election_setup.manifest_preview.property.numberOfStyles',
ElectionSetupManifestPreviewPropertyStartDate = 'election_setup.manifest_preview.property.start_date',
ElectionSetupManifestPreviewPropertyEndDate = 'election_setup.manifest_preview.property.end_date',
ElectionSetupManifestPreviewPropertyFileName = 'election_setup.manifest_preview.property.file_name',
ElectionSetupManifestPreviewCaption = 'election_setup.manifest_preview.caption',
ElectionSetupManifestPreviewNext = 'election_setup.manifest_preview.next',
ElectionSetupManifestPreviewBackToMenu = 'election_setup.manifest_preview.back_to_menu',
ElectionSetup_ManifestPreview_Title = 'election_setup.manifest_preview.title',
ElectionSetup_ManifestPreview_PropertyName = 'election_setup.manifest_preview.property.name',
ElectionSetup_ManifestPreview_PropertyNumberOfContests = 'election_setup.manifest_preview.property.number_of_contests',
ElectionSetup_ManifestPreview_PropertyNumberOfStyles = 'election_setup.manifest_preview.property.numberOfStyles',
ElectionSetup_ManifestPreview_PropertyStartDate = 'election_setup.manifest_preview.property.start_date',
ElectionSetup_ManifestPreview_PropertyEndDate = 'election_setup.manifest_preview.property.end_date',
ElectionSetup_ManifestPreview_PropertyFileName = 'election_setup.manifest_preview.property.file_name',
ElectionSetup_ManifestPreview_Next = 'election_setup.manifest_preview.next',
ElectionSetup_ManifestPreview_BackToMenu = 'election_setup.manifest_preview.back_to_menu',
ElectionSetup_ManifestPreview_SubmitError = 'election_setup.manifest_preview.submit_error',
ElectionSetupSetupCompleteTitle = 'election_setup.setup_complete.title',
ElectionSetupSetupCompleteNext = 'election_setup.setup_complete.next',

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

@ -38,9 +38,9 @@
"election_setup.manifest_preview.property.start_date": "Start Date",
"election_setup.manifest_preview.property.end_date": "End Date",
"election_setup.manifest_preview.property.file_name": "File Name",
"election_setup.manifest_preview.caption": "Preview of Manifest",
"election_setup.manifest_preview.next": "Submit",
"election_setup.manifest_preview.back_to_menu": "Cancel",
"election_setup.manifest_preview.submit_error": "An error occurred while attempting to save the election",
"election_setup.upload_manifest.title": "Upload Election Manifest",
"election_setup.upload_manifest.upload": "Select Files to Upload",

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

@ -1,13 +1,13 @@
import {
Body_login_for_access_token_api_v1_auth_login_post,
ErrorMessage,
ClientFactory,
Token,
} from '@electionguard/api-client';
import { Button, Container, InputAdornment, TextField } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { AccountCircle, Lock } from '@mui/icons-material';
import React, { useState } from 'react';
import { useAuthClient } from '../hooks/useClient';
const useStyles = makeStyles((theme) => ({
root: {
@ -44,9 +44,9 @@ export const LoginPage: React.FC<LoginPageProps> = ({ setToken }) => {
const [password, setPassword] = useState('');
const [result, setResult] = useState<string>();
const authClient = useAuthClient();
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
const authClient = ClientFactory.GetAuthClient();
const loginParams = {
username,
password,

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

@ -26,7 +26,7 @@ const AuthenticatedRoutes: React.FC = () => (
<Route path={routeIds.home} element={<Navigate to="/menu" />} />
<Route path="/menu" element={<MenuPage />} />
<Route path="/election" element={<ElectionListPage />} />
<Route path={routeIds.electionList} element={<ElectionListPage />} />
<Route path="/election-setup" element={<ElectionSetupPage />} />
<Route path="/election/:election-id/key" element={<ElectionKeyPage />} />
<Route path="/election/:election-id/upload-ballot" element={<UploadBallotPage />} />

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

@ -1,5 +1,6 @@
const routeIds = {
home: '/',
electionList: '/election',
};
export default routeIds;