diff --git a/apps/dashboard-for-dapps/src/components/grants-pagination.tsx b/apps/dashboard-for-dapps/src/components/grants-pagination.tsx new file mode 100644 index 000000000..66f418032 --- /dev/null +++ b/apps/dashboard-for-dapps/src/components/grants-pagination.tsx @@ -0,0 +1,24 @@ +import { HStack, PaginationNextTrigger, PaginationPrevTrigger } from "@chakra-ui/react"; +import { PaginationItems, PaginationRoot } from "./ui/pagination"; + +export default function GrantsPagination({ + count, + pageSize, + page, + setPage, +}: { count: number; pageSize: number; page: number; setPage: (page: number) => void }) { + return ( + setPage(page)} + > + + + + + + + ); +} diff --git a/apps/dashboard-for-dapps/src/components/ui/link-button.tsx b/apps/dashboard-for-dapps/src/components/ui/link-button.tsx new file mode 100644 index 000000000..90fc8819e --- /dev/null +++ b/apps/dashboard-for-dapps/src/components/ui/link-button.tsx @@ -0,0 +1,8 @@ +import type { HTMLChakraProps, RecipeProps } from "@chakra-ui/react"; +import { createRecipeContext } from "@chakra-ui/react"; + +export interface LinkButtonProps extends HTMLChakraProps<"a", RecipeProps<"button">> {} + +const { withContext } = createRecipeContext({ key: "button" }); + +export const LinkButton = withContext("a"); diff --git a/apps/dashboard-for-dapps/src/components/ui/pagination.tsx b/apps/dashboard-for-dapps/src/components/ui/pagination.tsx new file mode 100644 index 000000000..90bc4bb8d --- /dev/null +++ b/apps/dashboard-for-dapps/src/components/ui/pagination.tsx @@ -0,0 +1,188 @@ +import type { ButtonProps, TextProps } from "@chakra-ui/react"; +import { + Button, + Pagination as ChakraPagination, + IconButton, + Text, + createContext, + usePaginationContext, +} from "@chakra-ui/react"; +import * as React from "react"; +import { HiChevronLeft, HiChevronRight, HiMiniEllipsisHorizontal } from "react-icons/hi2"; +import { LinkButton } from "./link-button"; + +interface ButtonVariantMap { + current: ButtonProps["variant"]; + default: ButtonProps["variant"]; + ellipsis: ButtonProps["variant"]; +} + +type PaginationVariant = "outline" | "solid" | "subtle"; + +interface ButtonVariantContext { + size: ButtonProps["size"]; + variantMap: ButtonVariantMap; + getHref?: (page: number) => string; +} + +const [RootPropsProvider, useRootProps] = createContext({ + name: "RootPropsProvider", +}); + +export interface PaginationRootProps extends Omit { + size?: ButtonProps["size"]; + variant?: PaginationVariant; + getHref?: (page: number) => string; +} + +const variantMap: Record = { + outline: { default: "ghost", ellipsis: "plain", current: "outline" }, + solid: { default: "outline", ellipsis: "outline", current: "solid" }, + subtle: { default: "ghost", ellipsis: "plain", current: "subtle" }, +}; + +export const PaginationRoot = React.forwardRef( + function PaginationRoot(props, ref) { + const { size = "sm", variant = "outline", getHref, ...rest } = props; + return ( + + + + ); + }, +); + +export const PaginationEllipsis = React.forwardRef( + function PaginationEllipsis(props, ref) { + const { size, variantMap } = useRootProps(); + return ( + + + + ); + }, +); + +export const PaginationItem = React.forwardRef( + function PaginationItem(props, ref) { + const { page } = usePaginationContext(); + const { size, variantMap, getHref } = useRootProps(); + + const current = page === props.value; + const variant = current ? variantMap.current : variantMap.default; + + if (getHref) { + return ( + + {props.value} + + ); + } + + return ( + + + + ); + }, +); + +export const PaginationPrevTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPagination.PrevTriggerProps +>(function PaginationPrevTrigger(props, ref) { + const { size, variantMap, getHref } = useRootProps(); + const { previousPage } = usePaginationContext(); + + if (getHref) { + return ( + + + + ); + } + + return ( + + + + + + ); +}); + +export const PaginationNextTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPagination.NextTriggerProps +>(function PaginationNextTrigger(props, ref) { + const { size, variantMap, getHref } = useRootProps(); + const { nextPage } = usePaginationContext(); + + if (getHref) { + return ( + + + + ); + } + + return ( + + + + + + ); +}); + +export const PaginationItems = (props: React.HTMLAttributes) => { + return ( + + {({ pages }) => + pages.map((page, index) => { + return page.type === "ellipsis" ? ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + ) : ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + ); + }) + } + + ); +}; + +interface PageTextProps extends TextProps { + format?: "short" | "compact" | "long"; +} + +export const PaginationPageText = React.forwardRef( + function PaginationPageText(props, ref) { + const { format = "compact", ...rest } = props; + const { page, totalPages, pageRange, count } = usePaginationContext(); + const content = React.useMemo(() => { + if (format === "short") return `${page} / ${totalPages}`; + if (format === "compact") return `${page} of ${totalPages}`; + return `${pageRange.start + 1} - ${pageRange.end} of ${count}`; + }, [format, page, totalPages, pageRange, count]); + + return ( + + {content} + + ); + }, +); diff --git a/apps/dashboard-for-dapps/src/mokc/access-grants.ts b/apps/dashboard-for-dapps/src/mokc/access-grants.ts new file mode 100644 index 000000000..65750e473 --- /dev/null +++ b/apps/dashboard-for-dapps/src/mokc/access-grants.ts @@ -0,0 +1,124 @@ +interface AccessGrant { + dataId: string; + lockedUntil: number; + owner: string; + grantee: string; +} + +export const mockAccessGrants: AccessGrant[] = [ + { + dataId: "did:ethr:1x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:2x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:3x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:4x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:5x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:6x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:7x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:0x8a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:1x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:2x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:3x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:4x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:5x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:6x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:7x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:0x8a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, + { + dataId: "did:ethr:1x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + lockedUntil: 1672531200, + owner: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + grantee: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + }, +]; + +const mockWait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const mockFetchAccessGrants = async ( + page = 1, + perPage = 5, +): Promise<{ records: AccessGrant[]; totalRecords: number }> => { + await mockWait(500); + return { + records: [...mockAccessGrants].slice((page - 1) * perPage, page * perPage), + totalRecords: mockAccessGrants.length, + }; +}; diff --git a/apps/dashboard-for-dapps/src/routes/index.tsx b/apps/dashboard-for-dapps/src/routes/index.tsx index 465463430..dbd4518a1 100644 --- a/apps/dashboard-for-dapps/src/routes/index.tsx +++ b/apps/dashboard-for-dapps/src/routes/index.tsx @@ -45,9 +45,10 @@ import ascii85 from "ascii85"; import { matchSorter } from "match-sorter"; import { useMemo, useRef, useState } from "react"; import nacl from "tweetnacl"; -import { useAccount } from "wagmi"; +import GrantsPagination from "@/components/grants-pagination"; import { useIdOS } from "@/idOS.provider"; +import { mockFetchAccessGrants } from "@/mokc/access-grants"; export const Route = createFileRoute("/")({ component: Index, @@ -66,18 +67,13 @@ function transformBase85Image(src: string) { )}`; } -const useFetchGrants = () => { - const idOS = useIdOS(); - const { address } = useAccount(); - +const useFetchGrants = (page: number) => { return useQuery({ - queryKey: ["grants"], - queryFn: () => - idOS.grants.list({ - grantee: address, - }), - select: (data) => - data.map((grant) => ({ + queryKey: ["grants", page], + queryFn: () => mockFetchAccessGrants(page, 5), + select: (data) => ({ + ...data, + records: data.records.map((grant) => ({ ...grant, lockedUntil: grant.lockedUntil === 0 @@ -87,6 +83,7 @@ const useFetchGrants = () => { timeStyle: "short", }).format(grant.lockedUntil * 1000), })), + }), }); }; type GrantsWithFormattedLockedUntil = NonNullable["data"]>; @@ -331,6 +328,7 @@ function CredentialDetails({ role="button" transition="transform 0.2s" cursor="pointer" @@ -366,13 +364,17 @@ function CredentialDetails({ ); } -function SearchResults({ results }: { results: GrantsWithFormattedLockedUntil }) { +function SearchResults({ + results, + setPage, + page, +}: { results: GrantsWithFormattedLockedUntil; setPage: (page: number) => void; page: number }) { const [credentialId, setCredentialId] = useState(""); const [openSecretKeyPrompt, toggleSecretKeyPrompt] = useToggle(); const [openCredentialDetails, toggleCredentialDetails] = useToggle(); const [secretKey, setSecretKey] = useLocalStorage("SECRET_KEY", ""); - if (!results.length) { + if (!results.records.length) { return ; } @@ -394,7 +396,7 @@ function SearchResults({ results }: { results: GrantsWithFormattedLockedUntil }) return ( <> - {results.map((grant) => ( + {results.records.map((grant) => ( ))} + @@ -483,10 +486,11 @@ function SearchResults({ results }: { results: GrantsWithFormattedLockedUntil }) } function Index() { + const [page, setPage] = useState(1); const navigate = useNavigate({ from: Route.fullPath }); const { filter = "" } = Route.useSearch(); const debouncedSearchTerm = useDebounce(filter, 300); - const grants = useFetchGrants(); + const grants = useFetchGrants(page); const handleSearchChange = (e: React.ChangeEvent) => { const search = e.target.value; @@ -498,12 +502,16 @@ function Index() { }; const results = useMemo(() => { - if (!grants.data) return []; + if (!grants.data) return { records: [], totalRecords: 0 }; if (!debouncedSearchTerm) return grants.data; - return matchSorter(grants.data, debouncedSearchTerm, { + const sortedRecords = matchSorter(grants.data.records, debouncedSearchTerm, { keys: ["dataId", "owner", "grantee", "lockedUntil"], }); + return { + records: sortedRecords, + totalRecords: grants.data.totalRecords, + }; }, [debouncedSearchTerm, grants.data]); return ( @@ -541,7 +549,7 @@ function Index() { onClick={() => grants.refetch()} /> - + )} diff --git a/packages/idos-sdk-js/src/lib/enclave-providers/iframe-enclave.ts b/packages/idos-sdk-js/src/lib/enclave-providers/iframe-enclave.ts index ce34de948..4d5a4606e 100644 --- a/packages/idos-sdk-js/src/lib/enclave-providers/iframe-enclave.ts +++ b/packages/idos-sdk-js/src/lib/enclave-providers/iframe-enclave.ts @@ -111,10 +111,13 @@ export class IframeEnclave implements EnclaveProvider { } async #loadEnclave() { - const hasIframe = document.getElementById(this.iframe.id); - if (hasIframe) { + const container = document.querySelector(this.container); + const iframeElem = document.getElementById(this.iframe.id); + + if (iframeElem) { console.warn("An Iframe already exists in the container"); - return Promise.resolve(); + container?.removeChild(iframeElem); + return new Promise((resolve) => this.#loadEnclave().then(resolve)); } // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#directives @@ -149,7 +152,6 @@ export class IframeEnclave implements EnclaveProvider { this.iframe.style.setProperty(k, v); } - const container = document.querySelector(this.container); if (!container) throw new Error(`Can't find container with selector ${this.container}`); container.appendChild(this.iframe);