diff --git a/admin-ui/pages/elections/[id]/open-test.tsx b/admin-ui/pages/elections/[id]/open-test.tsx index 0488e7e..2da12c1 100644 --- a/admin-ui/pages/elections/[id]/open-test.tsx +++ b/admin-ui/pages/elections/[id]/open-test.tsx @@ -1,4 +1,4 @@ -import { Button, Grid, Slider, SliderThumb, Typography } from "@mui/material"; +import { Alert, Button, Grid, Slider, SliderThumb, Typography } from "@mui/material"; import LoggedInLayout from "layout/LoggedInLayout"; import type { NextPage } from "next"; import { useRouter } from "next/router"; @@ -15,8 +15,9 @@ import { import GC from "component/GC"; import GI from "component/GI"; import Loading from "component/Loading"; -import ElectionCard from "component/ElectionCard"; import { useNavigate } from "react-router-dom"; +import useCurrentElection from "hooks/useCurrentElection"; +import LoadingButton from "component/LoadingButton"; interface ThumbProps { children: ReactNode; @@ -40,13 +41,18 @@ const TestElection: NextPage = () => { const { id } = query; const electionId = Array.isArray(id) ? id[0] : id; - + const [currentElection, reloadCurrentElection, loadingCurrentElection] = useCurrentElection(); const [election, setElection] = useState>(null); + + const shouldShowSetCurrentBtn = !loadingCurrentElection && currentElection && currentElection?.electionId != electionId; + const [alertText, setAlertText] = useState(""); + const [maliciousRequest, setMaliciousRequest] = useState(false); const loadElection = async () => { if (electionId) { const resp = await getElection(electionId); setElection(resp); + } }; @@ -56,13 +62,38 @@ const TestElection: NextPage = () => { } }, [electionId]); + useEffect(() => { + const modesNotAllowedForTest = [ElectionStatus.archived, ElectionStatus.closed, ElectionStatus.open]; + if (election && modesNotAllowedForTest.includes(election.electionStatus)) { + // setAlertText(`${resp?.electionName} is ${resp.electionStatus} and so cannot be set to test mode.`) + setMaliciousRequest(true); + } + }, [election]); + const testElection = async () => { if (electionId) { - await setCurrentElection(electionId); - await openElectionTest(electionId); - router.push("/dashboard"); + try { + await openElectionTest(electionId); + router.push("/dashboard"); + } catch (e: any) { + console.error(e); + setAlertText(e?.data?.error_description); + } + // await setCurrentElection(electionId); + } + // loadElection(); + }; + + const runSetCurrentElection = async () => { + if (electionId) { + try { + await setCurrentElection(electionId); + reloadCurrentElection(); + } catch (e: any) { + console.error(e); + setAlertText(`${currentElection?.electionJurisdictionName} ${currentElection?.electionName} ${e?.data?.error_description}`); + } } - loadElection(); }; const navigate = useNavigate(); @@ -71,83 +102,131 @@ const TestElection: NextPage = () => { navigate(-1); }; - return ( - - {!election && } - {election && - election.latMode == - 1 /*election?.electionStatus === ElectionStatus.test */ && ( - + const InTestMode = () => { + return ( + + + You are now in Testing Mode! + + + You are now in testing mode! Please test your election with your + team, then come back to finish editing and launch your election! + + + - You are now in Testing Mode! + - - You are now in testing mode! Please test your election with your - team, then come back to finish editing and launch your election! - - - - - - - - - + - )} - {election && !election?.latMode && ( - // This should be latMode 0; but some elections don't have that? - //== 0 /*election?.electionStatus !== ElectionStatus.test */ && ( - <> - Please confirm to continue. - - Please confirm that you would like to enter testing mode for{" "} - {election?.electionName}. - - - + + + ) + } + + const NeedCurrentMode = () => { + return ( + + + Prerequisites not met for test mode! + + + Election for {election?.electionJurisdictionName} {election?.electionName} is not the current election and so cannot be set to test mode. + + + + - - -   - - - { - if (newValue === 100) { - testElection(); - } - }} - components={{ - Thumb: ThumbComponent, - }} - step={null} - marks={[ - { - value: 0, - label: "", - }, - { - value: 100, - label: "", - }, - ]} - defaultValue={0} - /> - + + {!alertText ? + + + Set Current + + + : false + } + + + + ) + } + + const SetTestMode = () => { + return ( + <> + Please confirm to continue. + + Please confirm that you would like to enter testing mode for{" "} + {election?.electionName}. + + + + + + +   + + { + if (newValue === 100) { + testElection(); + } + }} + components={{ + Thumb: ThumbComponent, + }} + step={null} + marks={[ + { + value: 0, + label: "", + }, + { + value: 100, + label: "", + }, + ]} + defaultValue={0} + /> + + + + ) + } + + return ( + + {(!election || loadingCurrentElection) && } + {maliciousRequest ? + {election?.electionName} is {election?.electionStatus} and so cannot be set to test mode. + : + <> + {!loadingCurrentElection && election && + election.latMode == 1 /*election?.electionStatus === ElectionStatus.test */ && + + } + {!loadingCurrentElection && election && !election?.latMode && ( + // This should be latMode 0; but some elections don't have that? + //== 0 /*election?.electionStatus !== ElectionStatus.test */ && ( + shouldShowSetCurrentBtn ? : + )} + + {alertText && {alertText}} + - )} + } ); }; diff --git a/admin-ui/pages/elections/[id]/set-current.tsx b/admin-ui/pages/elections/[id]/set-current.tsx new file mode 100644 index 0000000..a978f1b --- /dev/null +++ b/admin-ui/pages/elections/[id]/set-current.tsx @@ -0,0 +1,133 @@ +import { + Alert, + Button, + Grid, + Slider, + SliderThumb, + Typography, +} from "@mui/material"; +import LoggedInLayout from "layout/LoggedInLayout"; +import type { NextPage } from "next"; +import { useRouter } from "next/router"; +import { ReactNode, useEffect, useState } from "react"; + +import NavigateNextIcon from "@mui/icons-material/NavigateNext"; +import { Election, Maybe } from "types"; +import { + setCurrentElection, + getElection +} from "requests/election"; +import Loading from "component/Loading"; +import useCurrentElection from "hooks/useCurrentElection"; + +interface ThumbProps { + children: ReactNode; + [x: string]: any; +} + +function ThumbComponent(props: ThumbProps) { + const { children, ...other } = props; + + return ( + + {children} + + + ); +} + +const CurrentElection: NextPage = () => { + const router = useRouter(); + const { query } = router; + const { id } = query; + const [alertText, setAlertText] = useState(""); + const [currentElection, reloadCurrentElection] = useCurrentElection(); + + const setAlert = (text: string) => { + setAlertText(text); + setTimeout(() => setAlertText(""), 4000); + }; + + const electionId = Array.isArray(id) ? id[0] : id; + + const [election, setElection] = useState>(null); + + useEffect(() => { + const loadElection = async () => { + if (electionId) { + const resp = await getElection(electionId); + setElection(resp); + } + }; + if (electionId) { + loadElection(); + } + }, [electionId]); + + const runSetCurrentElection = async () => { + if (electionId) { + try { + await setCurrentElection(electionId); + router.push("/dashboard"); + } catch (e: any) { + console.error(e); + setAlert(e?.data?.error_description); + } + } + }; + + const archiveMessage = currentElection ? `This action will archive the election for ${currentElection?.electionJurisdictionName} ${currentElection.electionName}.` : ''; + + return ( + + {!election && } + {election && ( + <> + Please confirm to continue. + + Please confirm that you want to set the election for{" "} + {election?.electionJurisdictionName} {election?.electionName} as the current election. + {archiveMessage} + + + + + + +   + + + { + if (newValue === 100) { + runSetCurrentElection(); + } + }} + components={{ + Thumb: ThumbComponent, + }} + step={null} + marks={[ + { + value: 0, + label: "", + }, + { + value: 100, + label: "", + }, + ]} + defaultValue={0} + /> + + + + {alertText && {alertText}} + + + )} + + ); +}; + +export default CurrentElection; diff --git a/admin-ui/src/component/ConfirmationDialog.tsx b/admin-ui/src/component/ConfirmationDialog.tsx new file mode 100644 index 0000000..eb039dc --- /dev/null +++ b/admin-ui/src/component/ConfirmationDialog.tsx @@ -0,0 +1,62 @@ +import React, { ReactNode, useState } from "react"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import { styled } from "@mui/material/styles"; +import LoadingButton from "./LoadingButton"; + + +interface ConfirmationDialogProps { + title?: string; + open: boolean; + children: ReactNode; + btnCancelText?: string; + btnConfirmText?: string; + onClose: (confirmed: boolean) => void; +} + +export default function ConfirmationDialog({ + title, + children, + open, + btnCancelText = 'Cancel', + btnConfirmText = 'Confirm', + onClose, +}: ConfirmationDialogProps) { + + const handleConfirm = async () => { + await onClose(true); + }; + + const handleCancel = async () => { + onClose(false); + }; + + const ActionButton = styled(LoadingButton) ` + width: auto; + border-radius: 5px; + margin: 0 5px 10px 5px; + ` + + return ( + + + {title} + + + {children} + + + + {btnCancelText} + + + {btnConfirmText} + + + + ); +}; diff --git a/admin-ui/src/component/ElectionCard.tsx b/admin-ui/src/component/ElectionCard.tsx index a6a8011..398990c 100644 --- a/admin-ui/src/component/ElectionCard.tsx +++ b/admin-ui/src/component/ElectionCard.tsx @@ -184,9 +184,13 @@ export default function ElectionCard({ currentElection?.electionStatus == ElectionStatus.open } onClick={async () => { - await setCurrentElection(election.electionId); - if (onUpdateElection) { - onUpdateElection(); + if (currentElection) { + router.push(`/elections/${election.electionId}/set-current`); + } else { + await setCurrentElection(election.electionId); + if (onUpdateElection) { + onUpdateElection(); + } } }} > diff --git a/admin-ui/src/component/ElectionForm.tsx b/admin-ui/src/component/ElectionForm.tsx index 9ecb2a5..264c539 100644 --- a/admin-ui/src/component/ElectionForm.tsx +++ b/admin-ui/src/component/ElectionForm.tsx @@ -10,6 +10,7 @@ import { Alert, Link, } from "@mui/material"; +import CircularProgress from '@mui/material/CircularProgress'; import { Election, ElectionConfiguration, @@ -60,6 +61,9 @@ import Loading from "./Loading"; import { dateToYMD, formatTimeStamp } from "dsl/date"; import InputEnumSelect from "./InputEnumSelect"; import { eachHourOfInterval } from "date-fns"; +import ConfirmationDialog from "./ConfirmationDialog"; +import LoadingButton from "./LoadingButton"; +import useCurrentElection from "hooks/useCurrentElection"; interface ElectionFormProps { election: Maybe; @@ -74,8 +78,9 @@ export default function ElectionForm({ }: ElectionFormProps) { const [step, setStepData] = useState(0); const [data, setData] = useState>(election); - const [currentElection, setReferenceCurrentElection] = - useState>(); + const [currentElection, reloadCurrentElection, loadingCurrentElection] = useCurrentElection(); + // const [currentElection, setReferenceCurrentElection] = + // useState>(); const [alertText, setAlertText] = useState(""); const setStep = (step: number): void => { @@ -120,6 +125,8 @@ export default function ElectionForm({ election?.testVotersFile ? { status: "started" } : {} ); + const [currentElectionDialogOpen, setCurrentElectionDialogOpen] = useState(false); + const router = useRouter(); const steps = [ @@ -199,13 +206,50 @@ export default function ElectionForm({ handleDataChange(name, formattedDate); }; + const handleSetCurrentElection = async () => { + if (currentElection) { + setCurrentElectionDialogOpen(true); + } else { + await saveAsCurrentElection(); + } + }; + + const handleCurrentElectionDialogClose = async (confirmed: boolean) => { + + if (confirmed) { + await saveAsCurrentElection(); + } else { + setCurrentElectionDialogOpen(false); + } + + }; + + const saveAsCurrentElection = async () => { + if (election) { + try { + await setCurrentElection(election.electionId); + } catch (e) { + console.log("setCurrentElection error"); + console.log(e); + messageError(e); + } finally { + await reloadElectionData(); + reloadCurrentElection(); + setCurrentElectionDialogOpen(false); + } + } + } + + + const reloadElectionData = async () => { if (election?.electionId) { const updatedElection = await getElection(election?.electionId); setData(updatedElection); + onUpdateElection(updatedElection); } - const currentElection = await adminGetCurrentElection(); - setReferenceCurrentElection(currentElection); + // const currentElection = await adminGetCurrentElection(); + // setReferenceCurrentElection(currentElection); }; const save = async () => { @@ -939,7 +983,7 @@ export default function ElectionForm({ ); - + let formContents: ReactNode = null; if (step === 0) { formContents = electionNameFields; @@ -956,21 +1000,22 @@ export default function ElectionForm({ } else if (step === 6) { formContents = reviewFields; } + const actions = ( - + {step > 0 && ( - + )} {step < steps.length - 1 && step !== 5 && ( - + )} {step === 4 && @@ -990,14 +1035,16 @@ export default function ElectionForm({ }} > {(election?.testCount ?? 0) >= 1 ? "Continue" : "Begin"} Testing + + ) : ( step === 4 && ( - + {step === 4 && election && - currentElection && + !loadingCurrentElection && currentElection && currentElection?.electionId !== election.electionId && (

This election is not the current election and so cannot be set @@ -1010,28 +1057,30 @@ export default function ElectionForm({ (!election.electionStatus || election.electionStatus === ElectionStatus.draft) && (!currentElection || - (currentElection && + (!loadingCurrentElection && currentElection && currentElection?.electionId !== election?.electionId && currentElection?.latMode !== 1 && currentElection?.electionStatus !== ElectionStatus.open)) && ( - + + + + It will archive the election for {currentElection?.electionJurisdictionName} {currentElection?.electionName}. + + + )} ) @@ -1043,15 +1092,15 @@ export default function ElectionForm({ currentElection?.electionId === election.electionId && (election?.testCount ?? 0) >= 1 && !election?.testComplete && ( - + )} {step === steps.length - 1 && ( + + + ); +}; + diff --git a/admin-ui/src/hooks/useCurrentElection.ts b/admin-ui/src/hooks/useCurrentElection.ts index 3caa068..bebac5b 100644 --- a/admin-ui/src/hooks/useCurrentElection.ts +++ b/admin-ui/src/hooks/useCurrentElection.ts @@ -16,6 +16,7 @@ export default function useCurrentElection(): [ const [election, setElection] = useState>(null); const loadElection = async () => { + setLoading(true); let e = await adminGetCurrentElection(); //CTW shouldn't be doing it this way now diff --git a/lib/Election.js b/lib/Election.js index 021ada2..f3ca87c 100644 --- a/lib/Election.js +++ b/lib/Election.js @@ -1431,6 +1431,16 @@ class Election { case apiEndpoint.setCurrentElection: return Election.testConditions(endpoint, election, [ [liveCurrentElection, "Current election is in a live state"], + [ + electionAttributes?.["electionStatus"] == + Election.electionStatus.archived, + "Election has been archived.", + ], + [ + electionAttributes?.["electionStatus"] == + Election.electionStatus.closed, + "Election has been closed", + ] ]); case apiEndpoint.archiveElection: diff --git a/template.yaml b/template.yaml index 6ea095e..7cc1f41 100644 --- a/template.yaml +++ b/template.yaml @@ -960,6 +960,7 @@ Resources: CodeUri: endpoints/setElectionVoters/ Layers: - !Ref LibLayer + MemorySize: 512 Events: AbcBackendApiGateway: Type: Api