diff --git a/__tests__/pages/status.test.tsx b/__tests__/pages/status.test.tsx new file mode 100644 index 0000000..0cdb8ae --- /dev/null +++ b/__tests__/pages/status.test.tsx @@ -0,0 +1,132 @@ +// @ts-nocheck +import "@testing-library/jest-dom" +import { render, screen } from "@testing-library/react" +import Status from "../../pages/status" +import useWeb3AuthStore from "../../store/web3-auth" +import useOpenLoginSession from "../../hooks/useOpenLoginSession" +import { useFetchBalance, useFetchMetas } from "../../lib/ghostcloud" +import { useRouter } from "next/router" + +jest.mock("@chakra-ui/react", () => ({ + ...jest.requireActual("@chakra-ui/react"), + useTheme: () => ({ + colors: { + green: { + 400: "", + }, + red: { + 400: "", + }, + }, + }), +})) +jest.mock("react-query", () => ({ + useQuery: jest.fn(), +})) +jest.mock("../../hooks/useOpenLoginSession", () => jest.fn()) +jest.mock("../../lib/ghostcloud", () => ({ + useFetchBalance: jest.fn(() => ({ + error: null, + })), + useFetchMetas: jest.fn(() => ({ + data: { meta: [] }, + error: null, + isLoading: false, + refetch: jest.fn(), + })), +})) +jest.mock("../../store/web3-auth", () => jest.fn()) +jest.mock("next/router", () => ({ + useRouter: jest.fn().mockReturnValue({ + route: "/status", + pathname: "", + query: {}, + asPath: "", + push: jest.fn(), + }), +})) + +describe("Status", () => { + it("should redirect to homepage if not connected", async () => { + const pushMock = jest.fn() + useRouter.mockReturnValue({ push: pushMock }) + useWeb3AuthStore.mockReturnValue({ + isConnected: jest.fn().mockReturnValue(false), + }) + useFetchMetas.mockReturnValue([ + { data: { meta: [] }, isLoading: false, refetch: jest.fn() }, + ]) + render() + expect(pushMock).toHaveBeenCalledWith("/") + }) + + it("should show spinner if loading", async () => { + useWeb3AuthStore.mockReturnValue({ + isConnected: jest.fn().mockReturnValue(false), + }) + useOpenLoginSession.mockReturnValue(false) + useFetchMetas.mockReturnValue([ + { data: { meta: [] }, isLoading: true, refetch: jest.fn() }, + ]) + render() + expect(screen.getByText("Loading...")).toBeInTheDocument() + }) + + it("should render service status", async () => { + useWeb3AuthStore.mockReturnValue({ + isConnected: jest.fn().mockReturnValue(false), + }) + useOpenLoginSession.mockReturnValue(true) + useFetchMetas.mockReturnValue([ + { data: { meta: [] }, isLoading: false, refetch: jest.fn() }, + ]) + render() + expect(screen.getByText("All Systems Operational")).toBeInTheDocument() + }) + + it("should not render service status if balance error", async () => { + useWeb3AuthStore.mockReturnValue({ + isConnected: jest.fn().mockReturnValue(false), + }) + useOpenLoginSession.mockReturnValue(true) + useFetchBalance.mockReturnValue([{ error: new Error("error") }]) + useFetchMetas.mockReturnValue([ + { data: { meta: [] }, error: null, isLoading: false, refetch: jest.fn() }, + ]) + render() + expect(screen.getByText("All Systems Operational")).toBeInTheDocument() + }) + + it("should render deployment status", async () => { + useWeb3AuthStore.mockReturnValue({ + isConnected: jest.fn().mockReturnValue(true), + }) + useOpenLoginSession.mockReturnValue(true) + useFetchMetas.mockReturnValue([ + { + data: { meta: [{}], pagination: { total: 1 } }, + isLoading: false, + refetch: jest.fn(), + }, + ]) + render() + expect(screen.getByText("1 Deployment Active")).toBeInTheDocument() + }) + + it("should not render deployment status if metas error", async () => { + useWeb3AuthStore.mockReturnValue({ + isConnected: jest.fn().mockReturnValue(true), + }) + useOpenLoginSession.mockReturnValue(true) + useFetchMetas.mockReturnValue([ + { + data: { meta: [{}] }, + error: new Error("error"), + isLoading: false, + refetch: jest.fn(), + }, + ]) + render() + expect(screen.getByText("Deployments Degraded")).toBeInTheDocument() + }) +}) diff --git a/components/footer.tsx b/components/footer.tsx index f0e76e8..619aac8 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -9,11 +9,16 @@ import { useColorModeValue, } from "@chakra-ui/react" import Link from "next/link" +import useWeb3AuthStore from "../store/web3-auth" + import logoDark from "../public/logo-white.png" import logoLight from "../public/logo-black.png" import manifest from "../public/manifest-powered.webp" export default function Footer() { + const store = useWeb3AuthStore() + const isConnected = store.isConnected() + const bgColor = useColorModeValue( "modes.dark.background", "modes.light.background", @@ -42,6 +47,16 @@ export default function Footer() { Home + {isConnected && ( + <> + + Dashboard + + + Status + + + )} Terms Of Service diff --git a/lib/ghostcloud.ts b/lib/ghostcloud.ts index 63e54d0..c317011 100644 --- a/lib/ghostcloud.ts +++ b/lib/ghostcloud.ts @@ -268,7 +268,9 @@ export const useRemoveDeployment = () => { const pageLimit = 10 // Query the Ghostcloud RPC endpoint for deployments created by the current user -export const useFetchMetas = (): [ +export const useFetchMetas = ( + showError: boolean = true, +): [ UseQueryResult, number, number, @@ -315,7 +317,7 @@ export const useFetchMetas = (): [ queryKey: ["metas", page], queryFn: list, onError: error => { - displayError("Failed to fetch deployments", error as Error) + showError && displayError("Failed to fetch deployments", error as Error) }, keepPreviousData: true, }) @@ -331,7 +333,9 @@ export const useFetchMetas = (): [ return [query, page + 1, pageCount, handlePageClick] } -export const useFetchBalance = (): UseQueryResult => { +export const useFetchBalance = ( + showError: boolean = true, +): UseQueryResult => { const store = useWeb3AuthStore() const displayError = useDisplayError() @@ -362,7 +366,7 @@ export const useFetchBalance = (): UseQueryResult => { queryKey: "balance", queryFn: fetchBalance, onError: error => { - displayError("Failed to fetch balance", error) + showError && displayError("Failed to fetch balance", error) }, }) } diff --git a/pages/status.tsx b/pages/status.tsx new file mode 100644 index 0000000..03a9c2f --- /dev/null +++ b/pages/status.tsx @@ -0,0 +1,130 @@ +import { useFetchBalance, useFetchMetas } from "../lib/ghostcloud" +import { useEffect, useState } from "react" +import useWeb3AuthStore from "../store/web3-auth" +import useOpenLoginSession from "../hooks/useOpenLoginSession" +import { + Box, + Card, + Container, + Flex, + Heading, + Icon, + Spinner, + Text, + useTheme, +} from "@chakra-ui/react" +import { useRouter } from "next/router" +import { IoIosCheckmarkCircle, IoIosWarning } from "react-icons/io" + +interface StatusCardProps { + error: boolean + msg: string +} + +const StatusCard = ({ error, msg }: StatusCardProps) => { + const theme = useTheme() + + return ( + + + + + + + {msg} + + + + ) +} + +const Status = () => { + const store = useWeb3AuthStore() + const isConnected = store.isConnected() + const router = useRouter() + const [ + { + data: metas, + error: metaError, + isLoading: isMetaLoading, + refetch: refetchMetas, + }, + ] = useFetchMetas(false) + const { pagination: { total = 0 } = {} } = metas || {} + const [hasMetaError, setHasMetaError] = useState(false) + const [hasBalanceError, setHasBalanceError] = useState(false) + const [updated, setUpdated] = useState("") + const hasSession = useOpenLoginSession() + const { error: balanceError } = useFetchBalance(false) + + // Redirect if not logged in + useEffect(() => { + if (!isMetaLoading && !isConnected && !hasSession) { + router.push("/") + } + }, [isConnected, isMetaLoading, hasSession, router]) + + // Ensure that we get metas on refresh once we are connected + useEffect(() => { + if (hasSession && isConnected && !metas?.meta) { + refetchMetas() + setUpdated(new Date().toLocaleString()) + } + }, [hasSession, isConnected, metas, refetchMetas]) + + useEffect(() => { + setUpdated(new Date().toLocaleString()) + setHasBalanceError(!!balanceError) + }, [balanceError]) + + useEffect(() => { + setUpdated(new Date().toLocaleString()) + setHasMetaError(!!metaError) + }, [metaError]) + + if (!hasSession && !isConnected && isMetaLoading) { + return ( + + + + ) + } + + return ( +
+ + + + System Status + + Last Updated: {updated} + + + + {metas?.meta && metas.meta.length > 0 && ( + 1 ? "s" : "" + } Active` + } + /> + )} + + +
+ ) +} +export default Status