Skip to content

Commit

Permalink
Merge pull request sherlock-protocol#280 from sherlock-protocol/feat/…
Browse files Browse the repository at this point in the history
…admin

feat: admin panel
  • Loading branch information
frimoldi authored Jan 16, 2023
2 parents 7944f52 + 13b7481 commit 70d0462
Show file tree
Hide file tree
Showing 17 changed files with 884 additions and 47 deletions.
12 changes: 7 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { ProtocolPage } from "./pages/Protocol"
import { ClaimsPage } from "./pages/Claim"
import AppStakers from "./AppStakers"
import AppProtocols from "./AppProtocols"
import AppInternal from "./AppInternal"
import AppAdmin from "./AppAdmin"
import { AppContests } from "./AppContests"
import AppProtocolDashboard from "./AppProtocolDashboard"

import { routes, protocolsRoutes, contestsRoutes, internalRoutes, protocolDashboardRoutes } from "./utils/routes"
import { routes, protocolsRoutes, contestsRoutes, protocolDashboardRoutes, adminRoutes } from "./utils/routes"
import MobileBlock from "./components/MobileBlock/MobileBlock"
import { InternalOverviewPage } from "./pages/InternalOverview/InternalOverview"
import { ContestsPage } from "./pages/Contests"
Expand All @@ -27,6 +27,7 @@ import { useAccount } from "wagmi"
import { useAuthentication } from "./hooks/api/useAuthentication"
import { AuditPayments } from "./pages/AuditPayments/AuditPayments"
import { ProtocolTeam } from "./pages/ProtocolTeam/ProtocolTeam"
import { AdminContestsList } from "./pages/admin/AdminContestsList/AdminContestsList"

function App() {
const { address: connectedAddress } = useAccount()
Expand Down Expand Up @@ -97,10 +98,11 @@ function App() {
</Route>

{/** Internal section routes */}
<Route path={`${routes.Internal}/*`} element={<AppInternal />}>
<Route path={internalRoutes.InternalOverview} element={<InternalOverviewPage />} />
<Route path={`${routes.Admin}/*`} element={<AppAdmin />}>
<Route path={adminRoutes.InternalOverview} element={<InternalOverviewPage />} />
<Route path={adminRoutes.Contests} element={<AdminContestsList />} />

<Route path="*" element={<Navigate replace to={internalRoutes.InternalOverview} />} />
<Route path="*" element={<Navigate replace to={adminRoutes.InternalOverview} />} />
</Route>

<Route path="*" element={<Navigate replace to="/" />} />
Expand Down
70 changes: 70 additions & 0 deletions src/AppAdmin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useCallback, useEffect } from "react"
import { Outlet } from "react-router-dom"
import { Footer } from "./components/Footer"
import { Header, NavigationLink } from "./components/Header"

import styles from "./App.module.scss"
import { adminRoutes } from "./utils/routes"
import { useAdminProfile } from "./hooks/api/admin/useAdminProfile"
import { Button } from "./components/Button"
import { Box } from "./components/Box"
import { useAdminSignIn } from "./hooks/api/admin/useAdminSignIn"
import { ErrorModal } from "./pages/ContestDetails/ErrorModal"
import { useAccount } from "wagmi"
import { contests as contestsAPI } from "./hooks/api/axios"
import { adminSignOut as adminSignOutUrl } from "./hooks/api/urls"

const AppInternal = () => {
const { data: adminAddress } = useAdminProfile()
const { address: connectedAddress, isDisconnected } = useAccount()
const { signIn, error, reset } = useAdminSignIn()

useEffect(() => {
if (adminAddress && !connectedAddress && isDisconnected) {
contestsAPI.get(adminSignOutUrl())
}
}, [connectedAddress, adminAddress, isDisconnected])

const handleSignInAsAdmin = useCallback(() => {
signIn()
}, [signIn])

const handleErrorModalClose = useCallback(() => {
reset()
}, [reset])

const navigationLinks: NavigationLink[] = adminAddress
? [
{
title: "OVERVIEW",
route: adminRoutes.InternalOverview,
},
{
title: "CONTESTS",
route: adminRoutes.Contests,
},
]
: []

return (
<div className={styles.app}>
<div className={styles.noise} />
<Header navigationLinks={navigationLinks} />
<div className={styles.contentContainer}>
<div className={styles.content}>
{adminAddress ? (
<Outlet />
) : (
<Box shadow={false}>
<Button onClick={handleSignInAsAdmin}>Sign in as Admin</Button>
</Box>
)}
{error && <ErrorModal reason={error.fieldErrors} onClose={handleErrorModalClose} />}
</div>
</div>
<Footer />
</div>
)
}

export default AppInternal
31 changes: 0 additions & 31 deletions src/AppInternal.tsx

This file was deleted.

100 changes: 94 additions & 6 deletions src/AppProtocolDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,69 @@
import React from "react"
import React, { useCallback, useEffect, useState } from "react"
import { Outlet, useParams } from "react-router-dom"
import { FaCheck, FaGithub } from "react-icons/fa"
import { commify } from "ethers/lib/utils.js"
import { DateTime } from "luxon"

import { Footer } from "./components/Footer"
import { Header, NavigationLink } from "./components/Header"

import styles from "./App.module.scss"
import { protocolDashboardRoutes, routes } from "./utils/routes"
import { useProtocolDashboard } from "./hooks/api/contests/useProtocolDashboard"
import { Column, Row } from "./components/Layout"
import { Box } from "./components/Box"
import { Title } from "./components/Title"
import { Table, TBody, Td, Tr } from "./components/Table/Table"
import { commify } from "ethers/lib/utils.js"
import { DateTime } from "luxon"
import { Text } from "./components/Text"
import { Button } from "./components/Button"
import Modal, { Props as ModalProps } from "./components/Modal/Modal"
import { useFinalizeSubmission } from "./hooks/api/contests/useFinalizeSubmission"
import LoadingContainer from "./components/LoadingContainer/LoadingContainer"

type Props = ModalProps & {
dashboardID: string
}

const FinalizeSubmissionModal: React.FC<Props> = ({ onClose, dashboardID }) => {
const { finalizeSubmission, isLoading, isSuccess } = useFinalizeSubmission()

useEffect(() => {
if (isSuccess) {
onClose?.()
}
}, [isSuccess, onClose])

const handleCancelClick = useCallback(() => {
onClose?.()
}, [onClose])

const handleConfirmClick = useCallback(() => {
finalizeSubmission({ dashboardID })
}, [finalizeSubmission, dashboardID])

return (
<Modal closeable onClose={onClose}>
<LoadingContainer loading={isLoading}>
<Column spacing="xl">
<Title>Finalize submission</Title>
<Text>By confirming this action, the repo will become read-only.</Text>
<Text>Sherlock will review the details and confirm the start date soon.</Text>
<Text variant="secondary">This action cannot be undone.</Text>
<Row spacing="l" alignment={["center"]}>
<Button variant="secondary" onClick={handleCancelClick}>
Cancel
</Button>
<Button onClick={handleConfirmClick}>Confirm</Button>
</Row>
</Column>
</LoadingContainer>
</Modal>
)
}

const AppProtocolDashboard = () => {
const { dashboardID } = useParams()
const { data: protocolDashboard } = useProtocolDashboard(dashboardID ?? "")
const [finalizeSubmissionModalOpen, setFinalizeSubmissionModalOpen] = useState(false)

if (!protocolDashboard) return null

Expand All @@ -31,10 +78,11 @@ const AppProtocolDashboard = () => {
},
]

const contest = protocolDashboard.contest
const { contest, payments } = protocolDashboard
const startDate = DateTime.fromSeconds(contest.startDate)
const endDate = DateTime.fromSeconds(contest.endDate)
const length = endDate.diff(startDate, "days").days
const fullyPaid = payments.totalPaid >= payments.totalAmount

return (
<div className={styles.app}>
Expand All @@ -56,7 +104,7 @@ const AppProtocolDashboard = () => {
<TBody>
<Tr>
<Td>
<Text strong>Estimated Start Date</Text>
<Text strong>{contest.startApproved ? "Start Date" : "Estimated Start Date"}</Text>
</Td>
<Td>
<Text alignment="right">{startDate.toLocaleString(DateTime.DATE_MED)}</Text>
Expand Down Expand Up @@ -106,6 +154,40 @@ const AppProtocolDashboard = () => {
)}
</TBody>
</Table>
<Column spacing="s" alignment={["center"]}>
<Button variant="secondary" onClick={() => window.open(contest.repo, "blank")} fullWidth>
<FaGithub />
&nbsp;&nbsp;Audit repository
</Button>
{!contest.submissionReady && (
<Button
disabled={!fullyPaid || contest.submissionReady}
fullWidth
onClick={() => setFinalizeSubmissionModalOpen(true)}
>
Finalize submission
</Button>
)}
{!fullyPaid && (
<Text variant="secondary" size="small">
You need to submit the full payment
</Text>
)}
{contest.submissionReady && (
<Column spacing="s" alignment={["center"]}>
<Text variant="secondary">
<FaCheck /> Submission ready
</Text>
{contest.startApproved ? (
<Text variant="secondary" strong>
Contest start date approved
</Text>
) : (
<Text variant="secondary">Sherlock team will review it and confirm the start date</Text>
)}
</Column>
)}
</Column>
</Box>
</Column>
<Column grow={1}>
Expand All @@ -115,6 +197,12 @@ const AppProtocolDashboard = () => {
</div>
</div>
<Footer />
{finalizeSubmissionModalOpen && (
<FinalizeSubmissionModal
dashboardID={dashboardID ?? ""}
onClose={() => setFinalizeSubmissionModalOpen(false)}
/>
)}
</div>
)
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export const Table: React.FC<
selectable?: boolean
}
>
> = ({ children, selectable = true, ...props }) => (
<table className={cx(styles.table, { [styles.selectable]: selectable })} {...props}>
> = ({ children, selectable = true, className, ...props }) => (
<table className={cx(styles.table, className, { [styles.selectable]: selectable })} {...props}>
{children}
</table>
)
Expand Down
31 changes: 31 additions & 0 deletions src/hooks/api/admin/useAdminApproveContest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from "react-query"
import { adminApproveContest as adminApproveContestUrl } from "../urls"
import { contests as contestsAPI } from "../axios"
import { adminContestsQuery } from "./useAdminContests"

type AdminApproveContestParams = {
contestID: number
force: boolean
}

export const useAdminApproveContest = () => {
const queryClient = useQueryClient()
const { mutate, mutateAsync, ...mutation } = useMutation<void, Error, AdminApproveContestParams>(
async (params) => {
await contestsAPI.post(adminApproveContestUrl(), {
contest_id: params.contestID,
force: params.force,
})
},
{
onSuccess: async () => {
await queryClient.invalidateQueries(adminContestsQuery())
},
}
)

return {
approve: mutate,
...mutation,
}
}
31 changes: 31 additions & 0 deletions src/hooks/api/admin/useAdminApproveStart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from "react-query"
import { adminApproveStart as adminApproveStartUrl } from "../urls"
import { contests as contestsAPI } from "../axios"
import { adminContestsQuery } from "./useAdminContests"

type AdminApproveStartParams = {
contestID: number
force: boolean
}

export const useAdminApproveStart = () => {
const queryClient = useQueryClient()
const { mutate, mutateAsync, ...mutation } = useMutation<void, Error, AdminApproveStartParams>(
async (params) => {
await contestsAPI.post(adminApproveStartUrl(), {
contest_id: params.contestID,
force: params.force,
})
},
{
onSuccess: async () => {
await queryClient.invalidateQueries(adminContestsQuery())
},
}
)

return {
approve: mutate,
...mutation,
}
}
Loading

0 comments on commit 70d0462

Please sign in to comment.