diff --git a/README.md b/README.md index 25f7a38..abf1930 100644 --- a/README.md +++ b/README.md @@ -262,3 +262,15 @@ We recommend using the following IAM policy, which gives the least access possib ] } ``` +# Disaster Recovery + +During a deployment, there is a chance for a race condition between the CDK S3 deployer and CloudFront invalidation. +It is intermittent and is being tracked in [an issue in the cdk library](https://github.com/aws/aws-cdk/issues/15891). +If the deployment fails to update the Custom::CDKBucketDeployment, use the following process: + +1. Log into the effected AWS account and navigate to CloudFormation +2. Select the failed deployment and use the `Stack options` drop down to select `Continue update rollback` + 1. In the `Continue update rollback`, make sure to expand `Advanced` and select `skip` for the failing resource + 2. Trigger the rollback +3. Re-run the failed deployment from the CI/CD platform (GithubActions). This may require a second deployment to ensure the cache is invalidated + diff --git a/src/build-a-bull/.gitignore b/src/build-a-bull/.gitignore index 796af64..7f7e442 100644 --- a/src/build-a-bull/.gitignore +++ b/src/build-a-bull/.gitignore @@ -22,4 +22,5 @@ dist-ssr *.sln *.sw? -.env +.env* +!.env*.template diff --git a/src/build-a-bull/public/images/hero.png b/src/build-a-bull/public/images/hero.png index f59da99..b6d5057 100644 Binary files a/src/build-a-bull/public/images/hero.png and b/src/build-a-bull/public/images/hero.png differ diff --git a/src/build-a-bull/src/components/siteHeader.tsx b/src/build-a-bull/src/components/siteHeader.tsx index df9ed57..d66606f 100644 --- a/src/build-a-bull/src/components/siteHeader.tsx +++ b/src/build-a-bull/src/components/siteHeader.tsx @@ -19,11 +19,7 @@ interface Link { onClick?: () => void } -const createNavigation = () => - [ - { name: 'Home', href: '/', protect: false }, - { name: 'Create', href: '/create', protect: true }, - ] as Link[] +const createNavigation = () => [{ name: 'Create', href: '/create', protect: true }] as Link[] function NavLink(props: { currentClasses: string; defaultClasses: string; link: Link; displayName?: string }) { const classes = 'no-underline text-black' diff --git a/src/build-a-bull/src/features/rounds/index.tsx b/src/build-a-bull/src/features/rounds/index.tsx index 8a6cc90..82eb5ae 100644 --- a/src/build-a-bull/src/features/rounds/index.tsx +++ b/src/build-a-bull/src/features/rounds/index.tsx @@ -2,7 +2,7 @@ import { useWallet } from '@makerx/use-wallet' import { Alert, Button, Skeleton, Typography } from '@mui/material' import sortBy from 'lodash.sortby' import { useEffect, useState } from 'react' -import { Link } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import { VotingRoundGlobalState, fetchVotingRoundGlobalStatesByCreators } from '@/shared/VotingRoundContract' import { VoteType } from '@/shared/types' import { getHasVoteEnded, getHasVoteStarted } from '@/shared/vote' @@ -36,6 +36,14 @@ const VotingRounds = () => { const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) + // Redirect to live round + const navigate = useNavigate() + // TODO: add production voting round + const appId = window.location.hostname.includes('testnet') ? 499163907 : 1158913461 + if (import.meta.env.VITE_ENVIRONMENT !== 'local') { + // navigate(`/vote/${appId}`) + } + useEffect(() => { let addressesToFetch = [] as string[] if (showMyRounds && activeAddress) { diff --git a/src/build-a-bull/src/features/vote/VoteResults.tsx b/src/build-a-bull/src/features/vote/VoteResults.tsx index 3287bf8..f238ded 100644 --- a/src/build-a-bull/src/features/vote/VoteResults.tsx +++ b/src/build-a-bull/src/features/vote/VoteResults.tsx @@ -2,14 +2,12 @@ import FileDownloadIcon from '@mui/icons-material/FileDownload' import { Alert, Button, Skeleton, Typography } from '@mui/material' import { saveAs } from 'file-saver' import Papa from 'papaparse' -import { useState } from 'react' -import { Link } from 'react-router-dom' -import { VotingRoundMetadata } from '@/shared/IPFSGateway' +import { useMemo, useState } from 'react' +import { Question, VotingRoundMetadata } from '@/shared/IPFSGateway' import { VotingRoundGlobalState } from '@/shared/VotingRoundContract' import { ProposalCard } from '@/shared/ProposalCard' import { VotingRoundResult } from '@/shared/types' import { generateOptionIDsToCountsMapping } from '@/utils/common' -import { VoteDetails } from './VoteDetails' import VotingStats from './VotingStats' import { VotingTime } from './VotingTime' @@ -33,7 +31,25 @@ export const VoteResults = ({ const [error, setError] = useState(null) const optionIDsToCounts = votingRoundResults !== undefined ? generateOptionIDsToCountsMapping(votingRoundResults) : {} + function getTotalVotes() { + return votingRoundResults && votingRoundMetadata + ? votingRoundResults.reduce((c, r) => { + const isYesOption = votingRoundMetadata.questions.map((q) => q.options[0]).some((el) => el.id === r.optionId) + return isYesOption ? c + r.count : c + }, 0) + : 0 + } + + const countVotesTally = (question: Question) => { + return question.options.length > 0 && optionIDsToCounts[question.options[0].id] ? optionIDsToCounts[question.options[0].id] : 0 + } + const passedToTopSort = (q1: Question, q2: Question) => { + if (!q1.metadata?.threshold || !q2.metadata?.threshold) return 0 + return countVotesTally(q2) / q2.metadata.threshold - countVotesTally(q1) / q1.metadata.threshold + } + + const totalVotes = useMemo(() => getTotalVotes(), [votingRoundResults, votingRoundMetadata]) const generateProposalsResultsCsv = async () => { if (votingRoundMetadata) { setIsDownloadingProposalsCsv(true) @@ -67,23 +83,11 @@ export const VoteResults = ({ return (
-
- - < Back to Voting sessions - -
{votingRoundMetadata?.title} - Results
-
- -
+
))} {!isLoadingVotingRoundResults && - votingRoundMetadata?.questions.map((question) => ( -
- {question.metadata && ( - 0 && optionIDsToCounts[question.options[0].id] ? optionIDsToCounts[question.options[0].id] : 0 - } - hasClosed={true} - /> - )} -
- ))} + votingRoundMetadata?.questions + .sort((q1, q2) => passedToTopSort(q1, q2)) + .map((question) => ( +
+ {question.metadata && ( + + )} +
+ ))}
+
)} {votingRoundMetadata?.description && {votingRoundMetadata.description}} {votingRoundMetadata?.informationUrl && ( diff --git a/src/build-a-bull/src/shared/ProposalCard.tsx b/src/build-a-bull/src/shared/ProposalCard.tsx index e2378d5..c8273b8 100644 --- a/src/build-a-bull/src/shared/ProposalCard.tsx +++ b/src/build-a-bull/src/shared/ProposalCard.tsx @@ -16,6 +16,7 @@ export type ProposalCardProps = { totalVotes?: number | undefined hasClosed?: boolean forcePass?: boolean + skipTags?: boolean } export const ProposalCard = ({ @@ -30,6 +31,7 @@ export const ProposalCard = ({ totalVotes = 0, hasClosed = false, forcePass = false, + skipTags = false, }: ProposalCardProps) => { // Handle collapse state const [isOverflow, setIsOverflow] = useState(false) @@ -51,8 +53,8 @@ export const ProposalCard = ({
- {hasPassed && } - {hasClosed && !hasPassed && } + {hasPassed && !skipTags && } + {hasClosed && !hasPassed && !skipTags && }
diff --git a/src/xgov-dapp/.gitignore b/src/xgov-dapp/.gitignore index 796af64..7f7e442 100644 --- a/src/xgov-dapp/.gitignore +++ b/src/xgov-dapp/.gitignore @@ -22,4 +22,5 @@ dist-ssr *.sln *.sw? -.env +.env* +!.env*.template diff --git a/src/xgov-dapp/src/features/vote/VoteResults.tsx b/src/xgov-dapp/src/features/vote/VoteResults.tsx index 4e26031..1b0cde6 100644 --- a/src/xgov-dapp/src/features/vote/VoteResults.tsx +++ b/src/xgov-dapp/src/features/vote/VoteResults.tsx @@ -48,6 +48,19 @@ export const VoteResults = ({ const optionIDsToCounts = votingRoundResults !== undefined ? generateOptionIDsToCountsMapping(votingRoundResults) : {} + const countVotesTally = (question: Question) => { + return question.options.length > 0 && optionIDsToCounts[question.options[0].id] ? optionIDsToCounts[question.options[0].id] : 0 + } + + const passedToTopSort = (q1: Question, q2: Question) => { + if (!q1.metadata?.threshold || !q2.metadata?.threshold) return 0 + return countVotesTally(q2) / q2.metadata.threshold - countVotesTally(q1) / q1.metadata.threshold + } + + const passedToTopSortReserve = (q1: Question, q2: Question, passedReserveList: Set) => { + return passedReserveList.has(q2.id) ? 100 : passedReserveList.has(q1.id) ? -100 : passedToTopSort(q1, q2) + } + // clone the voting round metadata and adjust the threshold to be out of total votes instead of total voting power // we clone the metadata so that we don't mutate the original metadata const votingRoundMetadataClone = useMemo(() => { @@ -224,6 +237,7 @@ export const VoteResults = ({ ? !isReserveList(q, optionIDsToCounts[q.options[0].id]) : true, ) + .sort((q1, q2) => passedToTopSort(q1, q2)) .map((question) => (
{question.metadata && ( @@ -235,11 +249,7 @@ export const VoteResults = ({ link={question.metadata.link} threshold={question.metadata.threshold} ask={question.metadata.ask} - votesTally={ - question.options.length > 0 && optionIDsToCounts[question.options[0].id] - ? optionIDsToCounts[question.options[0].id] - : 0 - } + votesTally={countVotesTally(question)} hasClosed={true} /> )} @@ -265,28 +275,26 @@ export const VoteResults = ({ ))} {!isLoadingVotingRoundResults && - reserveList.map((question) => ( -
- {question.metadata && ( - 0 && optionIDsToCounts[question.options[0].id] - ? optionIDsToCounts[question.options[0].id] - : 0 - } - hasClosed={true} - forcePass={passedReserveList.has(question.id)} - /> - )} -
- ))} + reserveList + .sort((q1, q2) => passedToTopSortReserve(q1, q2, passedReserveList)) + .map((question) => ( +
+ {question.metadata && ( + + )} +
+ ))} )}