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);