Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create status page [42] #47

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions __tests__/pages/status.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Status />)
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(<Status />)
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(<Status />)
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(<Status />)
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(<Status />)
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(<Status />)
expect(screen.getByText("Deployments Degraded")).toBeInTheDocument()
})
})
15 changes: 15 additions & 0 deletions components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -42,6 +47,16 @@ export default function Footer() {
<Link href="/" passHref>
Home
</Link>
{isConnected && (
<>
<Link href="/dashboard" passHref>
Dashboard
</Link>
<Link href="/status" passHref>
Status
</Link>
</>
)}
<Link href="/terms" passHref>
Terms Of Service
</Link>
Expand Down
12 changes: 8 additions & 4 deletions lib/ghostcloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<QueryMetasResponse | undefined, unknown>,
number,
number,
Expand Down Expand Up @@ -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,
})
Expand All @@ -331,7 +333,9 @@ export const useFetchMetas = (): [
return [query, page + 1, pageCount, handlePageClick]
}

export const useFetchBalance = (): UseQueryResult<Coin, Error> => {
export const useFetchBalance = (
showError: boolean = true,
): UseQueryResult<Coin, Error> => {
const store = useWeb3AuthStore()
const displayError = useDisplayError()

Expand Down Expand Up @@ -362,7 +366,7 @@ export const useFetchBalance = (): UseQueryResult<Coin, Error> => {
queryKey: "balance",
queryFn: fetchBalance,
onError: error => {
displayError("Failed to fetch balance", error)
showError && displayError("Failed to fetch balance", error)
},
})
}
Expand Down
130 changes: 130 additions & 0 deletions pages/status.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card mb={5} p={5}>
<Flex align="center">
<Flex mr={2}>
<Icon
as={error ? IoIosWarning : IoIosCheckmarkCircle}
boxSize={7}
color={error ? theme.colors.red[400] : theme.colors.green[400]}
/>
</Flex>
<Heading as="h2" size="md" my={2}>
{msg}
</Heading>
</Flex>
</Card>
)
}

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 (
<Flex sx={{ height: "50vh" }} justify={"center"} align={"center"}>
<Spinner />
</Flex>
)
}

return (
<div>
<Container maxW="4xl" minH={"80vh"}>
<Box py={10}>
<Heading as="h1" size="xl" mt={6} mb={5}>
System Status
</Heading>
<Text mb={5}>Last Updated: {updated}</Text>

<StatusCard
error={hasBalanceError}
msg={
hasBalanceError ? "Systems Degraded" : "All Systems Operational"
}
/>

{metas?.meta && metas.meta.length > 0 && (
<StatusCard
error={hasMetaError}
msg={
hasMetaError
? "Deployments Degraded"
: `${total.toString()} Deployment${
total > 1 ? "s" : ""
} Active`
}
/>
)}
</Box>
</Container>
</div>
)
}
export default Status