diff --git a/CHANGELOG.md b/CHANGELOG.md index 08ab94281..fee94d409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ changes. ### Added +- added pagination to `drep/list` [Issue 756](https://github.com/IntersectMBO/govtool/issues/756) - added search query param to the `drep/getVotes` [Issue 640](https://github.com/IntersectMBO/govtool/issues/640) - added filtering and sorting capabilities to the `drep/list` [Issue 722](https://github.com/IntersectMBO/govtool/issues/722) - added drepView and txHash to the `ada-holder/get-current-delegation` [Issue 689](https://github.com/IntersectMBO/govtool/issues/689) @@ -55,9 +56,11 @@ changes. - Add frontend test workflow on github actions [Issue 500](https://github.com/IntersectMBO/govtool/issues/500) - Add type check & lint to github actions [Issue 512](https://github.com/IntersectMBO/govtool/issues/512) - Add eslint & prettier to frontend package [Issue 166](https://github.com/IntersectMBO/govtool/issues/166) +- Add DRep list pagination [Issue 740](https://github.com/IntersectMBO/govtool/issues/740) ### Fixed +- drep/list sql fix (now the latest tx date is correct) [Issue 826](https://github.com/IntersectMBO/govtool/issues/826) - drep/info no longer returns null values [Issue 720](https://github.com/IntersectMBO/govtool/issues/720) - drep/getVotes no longer returns 500 [Issue 685](https://github.com/IntersectMBO/govtool/issues/685) - drep/info no longer returns 500 [Issue 676](https://github.com/IntersectMBO/govtool/issues/676) @@ -109,6 +112,7 @@ changes. - Update frontend package readme to reflect recent changes [Issue 543](https://github.com/IntersectMBO/govtool/issues/543) - Change input selection strategy to 3 (random) [Issue 575](https://github.com/IntersectMBO/govtool/issues/575) - Changed documents to prepare for open source [Issue 737](https://github.com/IntersectMBO/govtool/issues/737) +- Changed copy on maintenance page [Issue 753](https://github.com/IntersectMBO/govtool/issues/753) ### Removed diff --git a/govtool/backend/sql/list-dreps.sql b/govtool/backend/sql/list-dreps.sql index 6a7351ec9..74def83c8 100644 --- a/govtool/backend/sql/list-dreps.sql +++ b/govtool/backend/sql/list-dreps.sql @@ -69,7 +69,7 @@ FROM LEFT JOIN voting_procedure AS voting_procedure ON voting_procedure.drep_voter = dh.id LEFT JOIN tx AS tx ON tx.id = voting_procedure.tx_id LEFT JOIN block AS block ON block.id = tx.block_id - JOIN ( + LEFT JOIN ( SELECT block.time, dr.drep_hash_id, @@ -79,9 +79,9 @@ FROM JOIN tx ON tx.id = dr.tx_id JOIN block ON block.id = tx.block_id WHERE - NOT (dr.deposit > 0)) AS newestRegister ON newestRegister.drep_hash_id = dh.id + NOT (dr.deposit < 0)) AS newestRegister ON newestRegister.drep_hash_id = dh.id AND newestRegister.rn = 1 - JOIN ( + LEFT JOIN ( SELECT dr.tx_id, dr.drep_hash_id, @@ -89,8 +89,8 @@ FROM FROM drep_registration dr) AS dr_first_register ON dr_first_register.drep_hash_id = dh.id AND dr_first_register.rn = 1 - JOIN tx AS tx_first_register ON tx_first_register.id = dr_first_register.tx_id - JOIN block AS block_first_register ON block_first_register.id = tx_first_register.block_id + LEFT JOIN tx AS tx_first_register ON tx_first_register.id = dr_first_register.tx_id + LEFT JOIN block AS block_first_register ON block_first_register.id = tx_first_register.block_id GROUP BY dh.raw, second_to_newest_drep_registration.voting_anchor_id, diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index 08789bbbf..d982c61dd 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -47,7 +47,9 @@ type VVAApi = :> QueryParam "search" Text :> QueryParams "status" DRepStatus :> QueryParam "sort" DRepSortMode - :> Get '[JSON] [DRep] + :> QueryParam "page" Natural + :> QueryParam "pageSize" Natural + :> Get '[JSON] ListDRepsResponse :<|> "drep" :> "get-voting-power" :> Capture "drepId" HexText :> Get '[JSON] Integer :<|> "drep" :> "getVotes" :> Capture "drepId" HexText @@ -120,8 +122,8 @@ delegationToResponse Types.Delegation {..} = } -drepList :: App m => Maybe Text -> [DRepStatus] -> Maybe DRepSortMode -> m [DRep] -drepList mSearchQuery statuses mSortMode = do +drepList :: App m => Maybe Text -> [DRepStatus] -> Maybe DRepSortMode -> Maybe Natural -> Maybe Natural -> m ListDRepsResponse +drepList mSearchQuery statuses mSortMode mPage mPageSize = do CacheEnv {dRepListCache} <- asks vvaCache dreps <- cacheRequest dRepListCache () DRep.listDReps @@ -148,7 +150,23 @@ drepList mSearchQuery statuses mSortMode = do dRepRegistrationStatus - return $ map drepRegistrationToDrep $ sortDReps $ filterDRepsByQuery $ filterDRepsByStatus dreps + let allValidDReps = map drepRegistrationToDrep $ sortDReps $ filterDRepsByQuery $ filterDRepsByStatus dreps + + + let page = (fromIntegral $ fromMaybe 0 mPage) :: Int + pageSize = (fromIntegral $ fromMaybe 10 mPageSize) :: Int + + total = length allValidDReps :: Int + + let elements = take pageSize $ drop (page * pageSize) allValidDReps + + return $ ListDRepsResponse + { listDRepsResponsePage = fromIntegral page + , listDRepsResponsePageSize = fromIntegral pageSize + , listDRepsResponseTotal = fromIntegral total + , listDRepsResponseElements = elements + } + getVotingPower :: App m => HexText -> m Integer getVotingPower (unHexText -> dRepId) = do diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index 4462f5a82..e911afa35 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -750,6 +750,42 @@ instance ToSchema DRep where & example ?~ toJSON exampleDrep + +exampleListDRepsResponse :: Text +exampleListDRepsResponse = + "{ \"page\": 0," + <> "\"pageSize\": 1," + <> "\"total\": 1000," + <> "\"elements\": [" + <> exampleDrep <> "]}" + +data ListDRepsResponse + = ListDRepsResponse + { listDRepsResponsePage :: Integer + , listDRepsResponsePageSize :: Integer + , listDRepsResponseTotal :: Integer + , listDRepsResponseElements :: [DRep] + } + deriving (Generic, Show) + +deriveJSON (jsonOptions "listDRepsResponse") ''ListDRepsResponse + +instance ToSchema ListDRepsResponse where + declareNamedSchema proxy = do + NamedSchema name_ schema_ <- + genericDeclareNamedSchema + ( fromAesonOptions $ + jsonOptions "listDRepsResponse" + ) + proxy + return $ + NamedSchema name_ $ + schema_ + & description ?~ "ListProposalsResponse" + & example + ?~ toJSON exampleListDRepsResponse + + data DelegationResponse = DelegationResponse { delegationResponseDRepHash :: Maybe HexText diff --git a/govtool/frontend/maintenance-page/index.html b/govtool/frontend/maintenance-page/index.html index f0438c102..47643b5ce 100644 --- a/govtool/frontend/maintenance-page/index.html +++ b/govtool/frontend/maintenance-page/index.html @@ -153,8 +153,12 @@

GovTool is down

- The SanchoNet GovTool is currently down for maintenance. Please try - again later. + The SanchoNet GovTool Beta is currently down for maintenance. + SanchoNet Govtool is connected to SanchoNet testnet, which + means that if the network gets upgraded with new features + Govtool needs to be updated too. + + Please try again later.

diff --git a/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx b/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx index 5a9e0ba3d..7734f9f30 100644 --- a/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx +++ b/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx @@ -4,7 +4,7 @@ import { Button, LoadingButton, Typography } from "@atoms"; import { primaryBlue } from "@consts"; import { useModal } from "@context"; import { useScreenDimension, useTranslation } from "@hooks"; -import { openInNewTab } from "@utils"; +import { openInNewTab, testIdFromLabel } from "@utils"; import { Card } from "./Card"; import { AutomatedVotingCardProps } from "./types"; @@ -24,6 +24,7 @@ export const AutomatedVotingCard = ({ const { isMobile, screenWidth } = useScreenDimension(); const { openModal } = useModal(); const { t } = useTranslation(); + const testIdLabel = testIdFromLabel(title); const onClickShowTransaction = () => openInNewTab(`https://sancho.cexplorer.io/tx/${transactionId}`); @@ -121,7 +122,7 @@ export const AutomatedVotingCard = ({ }} > + )} {status === "Active" && !isConnected && ( - )} diff --git a/govtool/frontend/src/components/organisms/DashboardCards.tsx b/govtool/frontend/src/components/organisms/DashboardCards.tsx index dcb2d48cc..779cb2276 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards.tsx @@ -42,46 +42,48 @@ export const DashboardCards = () => { } return ( - = 1728 - ? "repeat(3, minmax(300px, 570px))" - : "repeat(2, minmax(300px, 530px))", - justifyContent: screenWidth < 1024 ? "center" : "flex-start", - px: screenWidth < 640 ? 2 : 5, - py: 3, - rowGap: 3, - }} - > - + + = 1728 + ? "repeat(3, minmax(300px, 570px))" + : "repeat(2, minmax(300px, 530px))", + justifyContent: screenWidth < 1024 ? "center" : "flex-start", + px: screenWidth < 640 ? 2 : 5, + py: 3, + rowGap: 3, + }} + > + - + - + - + - + + ); }; diff --git a/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx index 0af605808..6aabe7efd 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx @@ -112,7 +112,7 @@ export const DelegateDashboardCard = ({ {displayedDelegationId && ( diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx index 426ad82b4..ee9729745 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx @@ -132,6 +132,7 @@ export const DashboardGovernanceActionDetails = () => { details={state ? state.details : data.proposal.details} url={state ? state.url : data.proposal.url} title={state ? state.title : data.proposal.title} + links={state ? state.references : data.proposal.references} about={state ? state.about : data.proposal.about} motivation={state ? state.motivation : data.proposal.motivation} rationale={state ? state.rationale : data.proposal.rationale} diff --git a/govtool/frontend/src/components/organisms/DashboardTopNav.tsx b/govtool/frontend/src/components/organisms/DashboardTopNav.tsx index fdf0a29c9..369483e7a 100644 --- a/govtool/frontend/src/components/organisms/DashboardTopNav.tsx +++ b/govtool/frontend/src/components/organisms/DashboardTopNav.tsx @@ -40,6 +40,7 @@ export const DashboardTopNav = ({ return ( <> ( { const onClickFeedback = () => openFeedbackWindow(); return ( - - - {t("footer.copyright")} - + <> - - - - - - + + + - + ); }; diff --git a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCard.tsx b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCard.tsx index d66a05a1c..7cc4656ee 100644 --- a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCard.tsx +++ b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCard.tsx @@ -24,6 +24,7 @@ type GovernanceActionDetailsCardProps = { motivation?: string; rationale?: string; yesVotes: number; + links?: string[]; govActionId: string; isDataMissing: boolean | MetadataValidationStatus; isDashboard?: boolean; @@ -46,6 +47,7 @@ export const GovernanceActionDetailsCard = ({ details, url, title, + links, about, motivation, rationale, @@ -91,23 +93,24 @@ export const GovernanceActionDetailsCard = ({ /> )} { const { t } = useTranslation(); const { screenWidth } = useScreenDimension(); @@ -121,7 +123,7 @@ export const GovernanceActionDetailsCardData = ({ {details && Object.keys(details).length !== 0 && ( )} - + ); }; diff --git a/govtool/frontend/src/components/organisms/Modal/StatusModal.tsx b/govtool/frontend/src/components/organisms/Modal/StatusModal.tsx index 95270c78c..ca5adb5d1 100644 --- a/govtool/frontend/src/components/organisms/Modal/StatusModal.tsx +++ b/govtool/frontend/src/components/organisms/Modal/StatusModal.tsx @@ -52,7 +52,15 @@ export const StatusModal = () => { {state?.message}{" "} {state?.link && ( diff --git a/govtool/frontend/src/consts/externalDataModalConfig.ts b/govtool/frontend/src/consts/externalDataModalConfig.ts index 132b20103..36dd10836 100644 --- a/govtool/frontend/src/consts/externalDataModalConfig.ts +++ b/govtool/frontend/src/consts/externalDataModalConfig.ts @@ -36,6 +36,7 @@ export const storageInformationErrorModals: Record< >["state"] > = { [MetadataValidationStatus.URL_NOT_FOUND]: urlCannotBeFound, + [MetadataValidationStatus.INCORRECT_FORMAT]: externalDataDoesntMatchModal, [MetadataValidationStatus.INVALID_JSONLD]: externalDataDoesntMatchModal, [MetadataValidationStatus.INVALID_HASH]: externalDataDoesntMatchModal, }; diff --git a/govtool/frontend/src/consts/queryKeys.ts b/govtool/frontend/src/consts/queryKeys.ts index 9a1e3c887..ccfdffe3f 100644 --- a/govtool/frontend/src/consts/queryKeys.ts +++ b/govtool/frontend/src/consts/queryKeys.ts @@ -1,7 +1,7 @@ export const QUERY_KEYS = { getAdaHolderCurrentDelegationKey: "getAdaHolderCurrentDelegationKey", getAdaHolderVotingPowerKey: "getAdaHolderVotingPowerKey", - useGetDRepListKey: "useGetDRepListKey", + useGetDRepListInfiniteKey: "useGetDRepListInfiniteKey", useGetDRepVotesKey: "useGetDRepVotesKey", useGetDRepVotingPowerKey: "useGetDRepVotingPowerKey", useGetProposalKey: "useGetProposalKey", diff --git a/govtool/frontend/src/hooks/forms/useDelegateTodRepForm.tsx b/govtool/frontend/src/hooks/forms/useDelegateTodRepForm.tsx index 12794fd79..7de68755c 100644 --- a/govtool/frontend/src/hooks/forms/useDelegateTodRepForm.tsx +++ b/govtool/frontend/src/hooks/forms/useDelegateTodRepForm.tsx @@ -4,7 +4,7 @@ import { useForm, useWatch } from "react-hook-form"; import { PATHS } from "@consts"; import { useCardano, useModal } from "@context"; -import { useGetDRepListQuery, useTranslation } from "@hooks"; +import { useGetDRepListInfiniteQuery, useTranslation } from "@hooks"; import { formHexToBech32 } from "@utils"; export interface DelegateTodrepFormValues { @@ -13,7 +13,9 @@ export interface DelegateTodrepFormValues { export const useDelegateTodRepForm = () => { const { buildSignSubmitConwayCertTx, buildVoteDelegationCert } = useCardano(); - const { data: drepList } = useGetDRepListQuery(); + const { dRepData: drepList } = useGetDRepListInfiniteQuery({ + page: 0, + }); const { openModal, closeModal, modal } = useModal(); const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); diff --git a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts index 47161bcc4..4ae33f960 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts @@ -1,35 +1,73 @@ -import { UseQueryOptions, useQuery } from "react-query"; +import { UseInfiniteQueryOptions, useInfiniteQuery } from "react-query"; import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; -import { GetDRepListParams, getDRepList } from "@services"; -import { DRepData } from "@/models"; +import { GetDRepListArguments, getDRepList } from "@services"; +import { InfinityDRepData } from "@/models"; -export const useGetDRepListQuery = ( - params?: GetDRepListParams, - options?: UseQueryOptions +export const useGetDRepListInfiniteQuery = ( + { + filters = [], + pageSize = 10, + searchPhrase, + sorting, + status, + }: GetDRepListArguments, + options?: UseInfiniteQueryOptions, ) => { - const { search, sort, status } = params || {}; const { pendingTransaction } = useCardano(); - const { data, isLoading, isPreviousData } = useQuery({ - queryKey: [ - QUERY_KEYS.useGetDRepListKey, - (pendingTransaction.registerAsSoleVoter || + const { + data, + isLoading, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + isPreviousData, + } = useInfiniteQuery( + [ + QUERY_KEYS.useGetDRepListInfiniteKey, + ( + pendingTransaction.registerAsSoleVoter || pendingTransaction.registerAsDrep || pendingTransaction.retireAsSoleVoter || - pendingTransaction.retireAsDrep)?.transactionHash, - search, - sort, + pendingTransaction.retireAsDrep + )?.transactionHash, + filters, + searchPhrase, + sorting, status, ], - queryFn: () => getDRepList({ - ...(search && { search }), - ...(sort && { sort }), - ...(status && { status }), - }), - ...options - }); + async ({ pageParam = 0 }) => + getDRepList({ + page: pageParam, + pageSize, + filters, + searchPhrase, + sorting, + status, + }), + { + getNextPageParam: (lastPage) => { + if (lastPage.elements.length === 0) { + return undefined; + } - return { data, isLoading, isPreviousData }; + return lastPage.page + 1; + }, + enabled: options?.enabled, + keepPreviousData: options?.keepPreviousData, + }, + ); + + return { + dRepListFetchNextPage: fetchNextPage, + dRepListHasNextPage: hasNextPage, + isDRepListFetching: isFetching, + isDRepListFetchingNextPage: isFetchingNextPage, + isDRepListLoading: isLoading, + dRepData: data?.pages.flatMap((page) => page.elements), + isPreviousData, + }; }; diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts index 7bde94cee..3bae61e10 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts @@ -24,11 +24,16 @@ export const useGetProposalsInfiniteQuery = ({ }); const mappedElements = await Promise.all( data.elements.map(async (proposal: ActionType) => { - const isDataMissing = await checkIsMissingGAMetadata({ + const { metadata, status } = await checkIsMissingGAMetadata({ hash: proposal?.metadataHash ?? "", url: proposal?.url ?? "", }); - return { ...proposal, isDataMissing }; + // workaround for the missing data in db-sync + return { + ...proposal, + ...metadata, + isDataMissing: status || false, + }; }), ); diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index 2bcb4f3c5..e6b0d6a38 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -33,11 +33,12 @@ export const useGetProposalsQuery = ({ allProposals .flatMap((proposal) => proposal.elements) .map(async (proposal) => { - const isDataMissing = await checkIsMissingGAMetadata({ + const { metadata, status } = await checkIsMissingGAMetadata({ hash: proposal?.metadataHash ?? "", url: proposal?.url ?? "", }); - return { ...proposal, isDataMissing }; + // workaround for the missing data in db-sync + return { ...proposal, ...metadata, isDataMissing: status || false }; }), ); diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index 979d0f6ca..e59acaa2a 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -751,7 +751,7 @@ export const en = { dataMissingErrors: { dataMissing: "Data Missing", notVerifiable: "Not Verifiable", - incorrectFormat: "Incorrect Format", + incorrectFormat: "Data Formatted Incorrectly", }, about: "About", abstain: "Abstain", diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index 5b72cff60..ba1b2eb28 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -18,6 +18,12 @@ export enum DRepStatus { Retired = "Retired", } +export enum DRepListSort { + VotingPower = "VotingPower", + RegistrationDate = "RegistrationDate", + Status = "Status", +} + export interface DRepData { drepId: string; view: string; @@ -28,6 +34,12 @@ export interface DRepData { status: DRepStatus; type: "DRep" | "SoleVoter"; } +export type InfinityDRepData = { + elements: DRepData[]; + page: number; + pageSize: number; + total: number; +}; export type Vote = "yes" | "no" | "abstain"; diff --git a/govtool/frontend/src/models/metadataValidation.ts b/govtool/frontend/src/models/metadataValidation.ts index e36e7c226..bb4a4edcc 100644 --- a/govtool/frontend/src/models/metadataValidation.ts +++ b/govtool/frontend/src/models/metadataValidation.ts @@ -3,11 +3,14 @@ export enum MetadataValidationStatus { URL_NOT_FOUND = "URL_NOT_FOUND", INVALID_JSONLD = "INVALID_JSONLD", INVALID_HASH = "INVALID_HASH", + INCORRECT_FORMAT = "INCORRECT_FORMAT", } export type ValidateMetadataResult = { status?: MetadataValidationStatus; valid: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata?: any; }; export enum MetadataStandard { diff --git a/govtool/frontend/src/pages/ChooseStakeKey.tsx b/govtool/frontend/src/pages/ChooseStakeKey.tsx index f8f4ae739..cdd2eac10 100644 --- a/govtool/frontend/src/pages/ChooseStakeKey.tsx +++ b/govtool/frontend/src/pages/ChooseStakeKey.tsx @@ -11,6 +11,8 @@ export const ChooseStakeKey = () => ( + {/* FIXME: Footer should be on top of the layout. + Should not be rerendered across the pages */}