diff --git a/app/components/DataTable/DataTable.tsx b/app/components/DataTable/DataTable.tsx index e2e4418d4..b7bb49f4f 100644 --- a/app/components/DataTable/DataTable.tsx +++ b/app/components/DataTable/DataTable.tsx @@ -2,8 +2,8 @@ import { createStyles } from "@mantine/core" import { Table, Box } from "@pokt-foundation/pocket-blocks" import { DataTableBody } from "./DataTableBody" import { DataTablePagination } from "./DataTablePagination" -import { DataTableProps, IdObj } from "~/components/DataTable/dataTable.d" import { usePagination } from "~/hooks/usePagination" +import { DataTableProps, IdObj } from "~/types/table" const useTableStyles = createStyles((theme) => ({ table: { @@ -13,6 +13,12 @@ const useTableStyles = createStyles((theme) => ({ "& thead tr th": { borderColor: "rgba(55,58,64, 0.5)", }, + "& tbody tr:hover": { + backgroundColor: + theme.colorScheme === "dark" + ? "rgba(37,38,43,0.50) !important" + : "rgba(250,250,250,0.50) !important", + }, }, })) @@ -22,6 +28,7 @@ export const DataTable = ({ paginate, rowAsLink = false, searchTerm, + onRowClick, }: DataTableProps) => { const { paginatedData, totalPages, page, handlePageChange } = usePagination({ data, @@ -33,7 +40,11 @@ export const DataTable = ({ return ( - +
{columns && ( @@ -44,7 +55,12 @@ export const DataTable = ({ )} - +
{paginate && ( diff --git a/app/components/DataTable/DataTableBody.tsx b/app/components/DataTable/DataTableBody.tsx index 429b09416..93ac27cca 100644 --- a/app/components/DataTable/DataTableBody.tsx +++ b/app/components/DataTable/DataTableBody.tsx @@ -2,13 +2,19 @@ import { Text } from "@pokt-foundation/pocket-blocks" import { Link } from "@remix-run/react" import { TableBodyProps, TableDataArray } from "~/types/table" -const renderTableCell = ([key, value]: TableDataArray) => ( - - {typeof value === "object" ? value.element : {value}} - -) +const renderTableCell = ([key, value]: TableDataArray) => + key !== "rowSelectData" ? ( + + {typeof value === "object" ? value.element : {value}} + + ) : null -export const DataTableBody = ({ paginatedData, rowAsLink, data }: TableBodyProps) => { +export const DataTableBody = ({ + paginatedData, + rowAsLink, + data, + onRowClick, +}: TableBodyProps) => { return ( {paginatedData.length > 0 ? ( @@ -17,7 +23,11 @@ export const DataTableBody = ({ paginatedData, rowAsLink, data }: TableBodyProps const tableData = Object.entries(itemData) return ( - + onRowClick(item.rowSelectData)} + > {rowAsLink ? ( { return ( - + { - title?: string - withSearch?: boolean - columns: (keyof T)[] - setSearchTerm: Dispatch> - rightComponent?: JSX.Element -} - -export interface TableBodyProps { - paginatedData: IdObj[] - rowAsLink?: boolean - data: IdObj[] -} - -export interface TablePaginationProps { - page: number - totalPages: number - onPageChange: (newPage: number) => void -} - -export interface DataTableProps { - data: T[] - columns?: Partial[] | string[] - label?: string - paginate: boolean | PaginateProps - rightComponent?: JSX.Element - subHeader?: JSX.Element - rowAsLink?: boolean - searchTerm?: string -} diff --git a/app/components/EmptyState/EmptyState.tsx b/app/components/EmptyState/EmptyState.tsx new file mode 100644 index 000000000..4b4e4ae93 --- /dev/null +++ b/app/components/EmptyState/EmptyState.tsx @@ -0,0 +1,43 @@ +import { Image, Text, Stack } from "@pokt-foundation/pocket-blocks" +import { type ReactNode } from "react" + +type EmptyStateProps = { + title: string + subtitle: ReactNode + imgSrc: string + imgHeight: number + imgWidth: number + alt?: string + callToAction?: ReactNode +} + +export const EmptyState = ({ + title, + subtitle, + imgSrc, + imgHeight, + imgWidth, + alt, + callToAction, +}: EmptyStateProps) => { + return ( + + {alt + + {title} + + + {subtitle} + + {callToAction ? callToAction : null} + + ) +} + +export default EmptyState diff --git a/app/routes/account.$accountId._index/components/EmptyState/index.ts b/app/components/EmptyState/index.ts similarity index 100% rename from app/routes/account.$accountId._index/components/EmptyState/index.ts rename to app/components/EmptyState/index.ts diff --git a/app/routes/account.$accountId.$appId.logs/components/LogsSideDrawer/LogsSideDrawer.tsx b/app/routes/account.$accountId.$appId.logs/components/LogsSideDrawer/LogsSideDrawer.tsx new file mode 100644 index 000000000..4f6032366 --- /dev/null +++ b/app/routes/account.$accountId.$appId.logs/components/LogsSideDrawer/LogsSideDrawer.tsx @@ -0,0 +1,82 @@ +import { Divider } from "@mantine/core" +import { Card, Drawer, Group, Stack, Text } from "@pokt-foundation/pocket-blocks" +import React from "react" +import { TitledCard } from "~/components/TitledCard" +import { Logs } from "~/models/dwh/sdk" +import { dayjs } from "~/utils/dayjs" + +type LogsSideDrawerProps = { + logsItem?: Logs + onSideDrawerClose: () => void +} + +const LogsSideDrawer = ({ logsItem, onSideDrawerClose }: LogsSideDrawerProps) => { + console.log({ logsItem }) + const cardItems = [ + { + label: "Date", + value: dayjs(logsItem?.ts).format("D MMMM, YYYY"), + }, + { + label: "Time", + value: dayjs(logsItem?.ts).format("h:mm A"), + }, + { + label: "Application ID", + value: logsItem?.portalApplicationId, + }, + { + label: "Chain ID", + value: logsItem?.chainId, + }, + { + label: "Supported method", + value: logsItem?.chainMethod, + }, + { + label: "Error type", + value: logsItem?.errorType, + }, + { + label: "Error name", + value: logsItem?.errorName, + }, + ] + + return ( + + + Summary}> + + + {cardItems.map(({ label, value }, index) => ( + + + {label} {value} + + {index !== cardItems.length - 1 && } + + ))} + + + + + Message}> + + {logsItem?.errorMessage} + + + + + ) +} + +export default LogsSideDrawer diff --git a/app/routes/account.$accountId.$appId.logs/components/LogsSideDrawer/index.ts b/app/routes/account.$accountId.$appId.logs/components/LogsSideDrawer/index.ts new file mode 100644 index 000000000..7ac825737 --- /dev/null +++ b/app/routes/account.$accountId.$appId.logs/components/LogsSideDrawer/index.ts @@ -0,0 +1,3 @@ +import LogsSideDrawer from "./LogsSideDrawer" +export * from "./LogsSideDrawer" +export default LogsSideDrawer diff --git a/app/routes/account.$accountId.$appId.logs/components/LogsTable/LogsTable.tsx b/app/routes/account.$accountId.$appId.logs/components/LogsTable/LogsTable.tsx new file mode 100644 index 000000000..ebbbe34eb --- /dev/null +++ b/app/routes/account.$accountId.$appId.logs/components/LogsTable/LogsTable.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react" +import { DataTable } from "~/components/DataTable" +import { Logs } from "~/models/dwh/sdk" +import LogsSideDrawer from "~/routes/account.$accountId.$appId.logs/components/LogsSideDrawer" +import { dayjs } from "~/utils/dayjs" + +type LogsTableProps = { + logs: Logs[] + searchTerm: string +} + +const LogsTable = ({ logs, searchTerm }: LogsTableProps) => { + const [selectedLogsItem, setSelectedLogsItem] = useState() + + return ( + <> + setSelectedLogsItem(undefined)} + /> + { + return { + timestamp: { + element: dayjs(log.ts).format("D MMMM, YYYY h:mm A"), + value: log.ts, + }, + method: { + element: log.chainMethod, + value: log.chainMethod, + }, + chainId: { + element: log.chainId, + value: log.chainId, + }, + errorType: { + element: log.errorType, + value: log.errorType, + }, + errorName: { + element: log.errorName, + value: log.errorName, + }, + rowSelectData: log, + } + })} + paginate={logs.length > 10 ? { perPage: 20 } : false} + searchTerm={searchTerm} + onRowClick={(logsItem) => setSelectedLogsItem(logsItem as unknown as Logs)} + /> + + ) +} + +export default LogsTable diff --git a/app/routes/account.$accountId.$appId.logs/components/LogsTable/index.ts b/app/routes/account.$accountId.$appId.logs/components/LogsTable/index.ts new file mode 100644 index 000000000..793d308ae --- /dev/null +++ b/app/routes/account.$accountId.$appId.logs/components/LogsTable/index.ts @@ -0,0 +1,3 @@ +import LogsTable from "./LogsTable" +export * from "./LogsTable" +export default LogsTable diff --git a/app/routes/account.$accountId.$appId.logs/route.tsx b/app/routes/account.$accountId.$appId.logs/route.tsx new file mode 100644 index 000000000..d6e3588ce --- /dev/null +++ b/app/routes/account.$accountId.$appId.logs/route.tsx @@ -0,0 +1,60 @@ +import { json, LoaderFunction, MetaFunction } from "@remix-run/node" +import { useLoaderData } from "@remix-run/react" +import invariant from "tiny-invariant" +import ErrorView from "~/components/ErrorView" +import { initDwhClient } from "~/models/dwh/dwh.server" +import { Logs } from "~/models/dwh/sdk/models/Logs" +import AppLogs from "~/routes/account.$accountId.$appId.logs/view" +import type { DataStruct } from "~/types/global" +import { getErrorMessage } from "~/utils/catchError" +import { seo_title_append } from "~/utils/seo" +import { requireUser } from "~/utils/user.server" + +export const meta: MetaFunction = () => { + return { + title: `Application Logs ${seo_title_append}`, + } +} + +type AppLogsData = { + logs: Logs[] +} + +export const loader: LoaderFunction = async ({ request, params }) => { + await requireUser(request) + const dwh = initDwhClient() + + try { + const { appId } = params + invariant(typeof appId === "string", "AppId must be a set url parameter") + + const logsResponse = await dwh.logsGet({ + portalApplicationId: [appId], + }) + + return json>({ + data: { + logs: logsResponse.data as Logs[], + }, + error: false, + }) + } catch (error) { + return json>({ + data: null, + error: true, + message: getErrorMessage(error), + }) + } +} + +export default function AccountInsights() { + const { data, error, message } = useLoaderData() as DataStruct + + if (error) { + return + } + + const { logs } = data + + return +} diff --git a/app/routes/account.$accountId.$appId.logs/view.tsx b/app/routes/account.$accountId.$appId.logs/view.tsx new file mode 100644 index 000000000..893bdb904 --- /dev/null +++ b/app/routes/account.$accountId.$appId.logs/view.tsx @@ -0,0 +1,56 @@ +import { Divider } from "@mantine/core" +import { useDebouncedValue } from "@mantine/hooks" +import { Box, Flex, Input, Text } from "@pokt-foundation/pocket-blocks" +import React, { useState } from "react" +import { LuSearch } from "react-icons/lu" +import { EmptyState } from "~/components/EmptyState" +import { Logs } from "~/models/dwh/sdk" +import LogsTable from "~/routes/account.$accountId.$appId.logs/components/LogsTable" + +type AppLogsProps = { + logs: Logs[] +} + +const AppLogs = ({ logs }: AppLogsProps) => { + const [searchTerm, setSearchTerm] = useState("") + const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 200) + + if (logs.length === 0) { + return ( + + They refresh hourly and are retained for 3 hours. +
+ Check back later for updates on any errors. + + } + title="No logs available yet." + /> + ) + } + + return ( + + + + Logs update hourly and are retained for a 3-hour window, ensuring you have the + most relevant data at your disposal. + + } + placeholder="Search logs" + value={searchTerm} + onChange={(event: any) => setSearchTerm(event.currentTarget.value)} + /> + + + + + ) +} + +export default AppLogs diff --git a/app/routes/account.$accountId.$appId/view.tsx b/app/routes/account.$accountId.$appId/view.tsx index af82d302a..f10b910f1 100644 --- a/app/routes/account.$accountId.$appId/view.tsx +++ b/app/routes/account.$accountId.$appId/view.tsx @@ -33,6 +33,10 @@ export default function AppIdLayoutView({ to: "insights", label: "Insights", }, + { + to: "logs", + label: "Logs", + }, { to: "security", label: "Security", diff --git a/app/routes/account.$accountId._index/components/EmptyState/EmptyState.tsx b/app/routes/account.$accountId._index/components/EmptyState/EmptyState.tsx deleted file mode 100644 index 0d3ce0bae..000000000 --- a/app/routes/account.$accountId._index/components/EmptyState/EmptyState.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Image, Text, Stack, Button } from "@pokt-foundation/pocket-blocks" -import { Link, useParams } from "@remix-run/react" - -export const EmptyState = () => { - const { accountId } = useParams() - - return ( - - Empty overview placeholder - - Create your first application - - - Applications connect your project to the blockchain.
- Set up your first one now. -
- -
- ) -} - -export default EmptyState diff --git a/app/routes/account.$accountId._index/route.tsx b/app/routes/account.$accountId._index/route.tsx index 25b5ff7b1..f1d1ca7c9 100644 --- a/app/routes/account.$accountId._index/route.tsx +++ b/app/routes/account.$accountId._index/route.tsx @@ -1,8 +1,9 @@ +import { Button } from "@pokt-foundation/pocket-blocks" import { json, LoaderFunction, MetaFunction } from "@remix-run/node" -import { useLoaderData } from "@remix-run/react" +import { Link, useLoaderData, useParams } from "@remix-run/react" import React from "react" import invariant from "tiny-invariant" -import { EmptyState } from "./components/EmptyState" +import { EmptyState } from "~/components/EmptyState" import ErrorView from "~/components/ErrorView" import { getAggregateRelays, getTotalRelays } from "~/models/dwh/dwh.server" import { AnalyticsRelaysAggregated } from "~/models/dwh/sdk/models/AnalyticsRelaysAggregated" @@ -75,6 +76,7 @@ export const loader: LoaderFunction = async ({ request, params }) => { export default function AccountInsights() { const { data, error, message } = useLoaderData() as DataStruct + const { accountId } = useParams() if (error) { return @@ -82,7 +84,32 @@ export default function AccountInsights() { const apps = data?.account?.portalApps as PortalApp[] - if (apps.length === 0) return + if (apps.length === 0) + return ( + + New Application + + } + imgHeight={205} + imgSrc="/overview-empty-state.svg" + imgWidth={122} + subtitle={ + <> + Applications connect your project to the blockchain.
+ Set up your first one now. + + } + title="Create your first application" + /> + ) return } diff --git a/app/routes/account_.$accountId.app-limit-exceeded/route.tsx b/app/routes/account_.$accountId.app-limit-exceeded/route.tsx index cfc136959..1c9850dfb 100644 --- a/app/routes/account_.$accountId.app-limit-exceeded/route.tsx +++ b/app/routes/account_.$accountId.app-limit-exceeded/route.tsx @@ -1,7 +1,8 @@ -import { Button, CloseButton, Image, Stack, Text } from "@pokt-foundation/pocket-blocks" +import { Button, CloseButton, Stack } from "@pokt-foundation/pocket-blocks" import { LoaderFunction, MetaFunction, redirect } from "@remix-run/node" import { Link, NavLink, useParams } from "@remix-run/react" import invariant from "tiny-invariant" +import { EmptyState } from "~/components/EmptyState" import { initPortalClient } from "~/models/portal/portal.server" import useCommonStyles from "~/styles/commonStyles" import { MAX_USER_APPS } from "~/utils/planUtils" @@ -54,35 +55,35 @@ export default function AppLimitExceeded() { size="lg" to="/account" /> - Empty overview placeholder + Discard + + } + imgHeight={216} + imgSrc="/app-limit-exceeded.svg" + imgWidth={270} + subtitle={ + <> + At the moment, we're limiting users to two applications as we're gearing up + for a major release
+ that will bring exciting new features and improvements. + + } + title="Currently you are only able to create two applications." /> - - Currently you are only able to create two applications. - - - At the moment, we're limiting users to two applications as we're gearing up for a - major release
- that will bring exciting new features and improvements. -
- ) } diff --git a/app/types/table.d.ts b/app/types/table.d.ts index 0221c8a55..61646761c 100644 --- a/app/types/table.d.ts +++ b/app/types/table.d.ts @@ -1,5 +1,3 @@ -import { Dispatch, SetStateAction } from "react" - export interface IdObj { [key: string]: any } @@ -12,18 +10,8 @@ export type PaginateProps = { export type TableDataArray = [string, any] -export interface TableHeaderProps { - label?: string - search?: boolean - columns: (keyof T)[] - setSearchTerm: Dispatch> - rightComponent?: JSX.Element -} - -export interface TableBodyProps { +export type TableBodyProps = Pick & { paginatedData: IdObj[] - rowAsLink?: boolean - data: IdObj[] } export interface TablePaginationProps { @@ -32,13 +20,12 @@ export interface TablePaginationProps { onPageChange: (newPage: number) => void } -export interface TableProps { +export interface DataTableProps { data: T[] columns?: Partial[] | string[] label?: string paginate: boolean | PaginateProps - search?: boolean - rightComponent?: JSX.Element - subHeader?: JSX.Element rowAsLink?: boolean + searchTerm?: string + onRowClick?: (item: T) => void } diff --git a/public/logs-empty-state.svg b/public/logs-empty-state.svg new file mode 100644 index 000000000..2b2186dc4 --- /dev/null +++ b/public/logs-empty-state.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +