Put Election API Call (#127)
* 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:
Родитель
ac5e56a025
Коммит
b5803475f5
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче