From 737068d5070b3c20ea1cc53b16fe787b8e763ee0 Mon Sep 17 00:00:00 2001 From: Renu Date: Wed, 15 Mar 2023 19:13:23 +0530 Subject: [PATCH 1/5] Add confirmation dialog when replacing a current Election --- admin-ui/pages/elections/[id]/set-current.tsx | 133 ++++++++++++++++++ admin-ui/src/component/ElectionCard.tsx | 10 +- db-json/create-ballots-table.json | 29 ++++ db-json/create-precincts-table.json | 29 ++++ lib/Election.js | 2 + 5 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 admin-ui/pages/elections/[id]/set-current.tsx create mode 100644 db-json/create-ballots-table.json create mode 100644 db-json/create-precincts-table.json 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..6412d1f --- /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?.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/ElectionCard.tsx b/admin-ui/src/component/ElectionCard.tsx index 1237746..7288d61 100644 --- a/admin-ui/src/component/ElectionCard.tsx +++ b/admin-ui/src/component/ElectionCard.tsx @@ -183,9 +183,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/db-json/create-ballots-table.json b/db-json/create-ballots-table.json new file mode 100644 index 0000000..5281f1e --- /dev/null +++ b/db-json/create-ballots-table.json @@ -0,0 +1,29 @@ +{ + "TableName": "abc_election_ballots_local", + "KeySchema": [{ + "AttributeName": "electionId_ballotId", + "KeyType": "HASH" + }], + "AttributeDefinitions": [{ + "AttributeName": "electionId_ballotId", + "AttributeType": "S" + }, + { + "AttributeName": "electionId", + "AttributeType": "S" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "electionId-index", + "Projection": { + "ProjectionType": "ALL" + }, + "KeySchema": [{ + "KeyType": "HASH", + "AttributeName": "electionId" + }] + } + ], + "BillingMode": "PAY_PER_REQUEST" +} \ No newline at end of file diff --git a/db-json/create-precincts-table.json b/db-json/create-precincts-table.json new file mode 100644 index 0000000..c00c8c6 --- /dev/null +++ b/db-json/create-precincts-table.json @@ -0,0 +1,29 @@ +{ + "TableName": "abc_election_precincts_local", + "KeySchema": [{ + "AttributeName": "electionId_precinctId", + "KeyType": "HASH" + }], + "AttributeDefinitions": [{ + "AttributeName": "electionId_precinctId", + "AttributeType": "S" + }, + { + "AttributeName": "electionId", + "AttributeType": "S" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "electionId-index", + "Projection": { + "ProjectionType": "ALL" + }, + "KeySchema": [{ + "KeyType": "HASH", + "AttributeName": "electionId" + }] + } + ], + "BillingMode": "PAY_PER_REQUEST" +} \ No newline at end of file diff --git a/lib/Election.js b/lib/Election.js index 4c151c4..5edbb2c 100644 --- a/lib/Election.js +++ b/lib/Election.js @@ -30,6 +30,8 @@ let documentBase; if (process.env.AWS_SAM_LOCAL) { tableName = `abc_elections_local`; voterTableName = "abc_voters_local"; + ballotsTableName = "abc_election_ballots_local"; + precinctsTableName = "abc_election_precincts_local"; // Allow local dev override //documentBucket = ""; //uploadBucket = ""; From cf8401503c571315adc08d58aa5a41d319ec4ecc Mon Sep 17 00:00:00 2001 From: Renu Date: Wed, 15 Mar 2023 19:17:14 +0530 Subject: [PATCH 2/5] update confirmation message --- admin-ui/pages/elections/[id]/set-current.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin-ui/pages/elections/[id]/set-current.tsx b/admin-ui/pages/elections/[id]/set-current.tsx index 6412d1f..a978f1b 100644 --- a/admin-ui/pages/elections/[id]/set-current.tsx +++ b/admin-ui/pages/elections/[id]/set-current.tsx @@ -85,7 +85,7 @@ const CurrentElection: NextPage = () => { <> Please confirm to continue. - Please confirm that you want to set the{" "} + Please confirm that you want to set the election for{" "} {election?.electionJurisdictionName} {election?.electionName} as the current election. {archiveMessage} From c0d8876af6a62650ad6b02fcfb7681fef158af95 Mon Sep 17 00:00:00 2001 From: Renu Date: Fri, 17 Mar 2023 23:08:04 +0530 Subject: [PATCH 3/5] Add confirmation dialog on new election page, add loaders on button --- admin-ui/src/component/ConfirmationDialog.tsx | 62 ++++++++++ admin-ui/src/component/ElectionForm.tsx | 109 +++++++++++++----- admin-ui/src/component/LoadingButton.tsx | 42 +++++++ admin-ui/src/hooks/useCurrentElection.ts | 1 + examples/edf-validation/.DS_Store | Bin 6148 -> 6148 bytes local-env-osx.json | 4 +- template.yaml | 3 +- 7 files changed, 188 insertions(+), 33 deletions(-) create mode 100644 admin-ui/src/component/ConfirmationDialog.tsx create mode 100644 admin-ui/src/component/LoadingButton.tsx 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/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/examples/edf-validation/.DS_Store b/examples/edf-validation/.DS_Store index e447cce5b73734f190284e91b3c594cad6415344..bed5ad0fe6350ead4b0bb4b3e8b1cba6eed6fd47 100644 GIT binary patch delta 288 zcmZoMXfc@JFUroqz`)4BAi&_6lb@WFlb;0S3v3o-Ue2fql44;Ee9B1 diff --git a/local-env-osx.json b/local-env-osx.json index 2b68c01..a798bb4 100644 --- a/local-env-osx.json +++ b/local-env-osx.json @@ -1,7 +1,7 @@ { "Parameters": { "DEV_ENVIRONMENT": "OSX", - "UPLOAD_BUCKET": "abc-uploads-development-vadminui", - "ELECTIONS_DOCUMENT_BUCKET": "abc-documents-development-vadminui" + "UPLOAD_BUCKET": "abc-uploads-development-radmin", + "ELECTIONS_DOCUMENT_BUCKET": "abc-documents-development-radmin" } } \ No newline at end of file diff --git a/template.yaml b/template.yaml index d7c9976..2a86e16 100644 --- a/template.yaml +++ b/template.yaml @@ -10,7 +10,7 @@ Parameters: # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: - Timeout: 15 + Timeout: 300 Handler: app.lambdaHandler Runtime: nodejs18.x Environment: @@ -941,6 +941,7 @@ Resources: CodeUri: endpoints/setElectionVoters/ Layers: - !Ref LibLayer + MemorySize: 512 Events: AbcBackendApiGateway: Type: Api From 5900a4d2d2c65d931e2cfc2d990c13ee403fac21 Mon Sep 17 00:00:00 2001 From: Renu Date: Fri, 24 Mar 2023 22:29:36 +0530 Subject: [PATCH 4/5] Fix issue 113 --- admin-ui/pages/elections/[id]/open-test.tsx | 231 +++++++++++++------- lib/Election.js | 10 + 2 files changed, 165 insertions(+), 76 deletions(-) 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/lib/Election.js b/lib/Election.js index 5edbb2c..42da46c 100644 --- a/lib/Election.js +++ b/lib/Election.js @@ -1433,6 +1433,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.openElectionTest: From 2b2daf0fbc8728ed9d32070810291d4fc4f16500 Mon Sep 17 00:00:00 2001 From: rgunpal Date: Thu, 6 Apr 2023 16:32:24 +0530 Subject: [PATCH 5/5] Delete .DS_Store --- examples/edf-validation/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 examples/edf-validation/.DS_Store diff --git a/examples/edf-validation/.DS_Store b/examples/edf-validation/.DS_Store deleted file mode 100644 index bed5ad0fe6350ead4b0bb4b3e8b1cba6eed6fd47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKL2KJE6n;vxIVKcx*r1nzq1TY6*(j7<+%zrpT95V6mdvib&6{V1*lD04;Pd*2 z`b+j-cG|utsi8?*=xK~i51u~h={?E%WF$Qz68%ZMP1GWy29&YpqIf{KpY@*9jKu*8 z9^)vJ7f~i7w0S@)e?J4fcXf&>q>{pw{o6xd=a&+-kcD3~EkolG7;Io_tWmry%emFkm);lS)=(Zk4@6%*DY`xf*c^Rg8GFAa;oWSMmB+X+v z?aN6X7b-T;9j@DOhpmm-thc?>_If+7=WTDc+v~QyH@m(0+-#>-%wXEOf;8xPW?fD#&krUM$#A$91AQW`B&CSJkw zE@ck<6Dm-4=nEAq-uw}Fz1Qgy8Ek2w{9K7d(~4!lGGG~aIs@ixY&4%9 zVm&PbmVy6-0X`o*D5Gz%(x{FO6e