diff --git a/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx b/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx index 457b71c24a..05862d55bf 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx @@ -2,6 +2,7 @@ import { useRoutes, BrowserRouter, RouteObject } from "react-router-dom"; import CssBaseline from "@mui/material/CssBaseline"; import { ThemeProvider, createTheme } from "@mui/material/styles"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +// import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { themeOptions } from "./theme"; import ContentLayout from "./components/Layout/ContentLayout"; @@ -9,6 +10,7 @@ import HeaderBar from "./components/Layout/HeaderBar"; import WelcomePage from "./components/WelcomePage"; import { AppConfig, AppListEntry } from "./common/types/app"; import { patchAppRoutePath } from "./common/utils"; +import { NotificationProvider } from "./common/context/NotificationContext"; type AppConfigProps = { appConfig: AppConfig[]; @@ -117,9 +119,11 @@ const CactiLedgerBrowserApp: React.FC = ({ appConfig }) => { - - - {/* */} + + + + {/* */} + diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/BlockList/BlockList.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/BlockList/BlockList.tsx new file mode 100644 index 0000000000..c5b12a14cb --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/BlockList/BlockList.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import { ethereumAllBlocksQuery } from "../../queries"; +import { blockColumnsConfig } from "./blockColumnsConfig"; +import type { UITableListingPaginationActionProps } from "../../../../components/ui/UITableListing/UITableListingPaginationAction"; +import UITableListing from "../../../../components/ui/UITableListing/UITableListing"; + +/** + * List of columns that can be rendered in a block list table + */ +export type BlockListColumn = keyof typeof blockColumnsConfig; + +/** + * BlockList properties. + */ +export interface BlockListProps { + footerComponent: React.ComponentType; + columns: BlockListColumn[]; + rowsPerPage: number; + tableSize?: "small" | "medium"; +} + +/** + * BlockList - Show table with ethereum blocks. + * + * @param footerComponent component will be rendered in a footer of a transaction list table. + * @param columns list of columns to be rendered. + * @param rowsPerPage how many rows to show per page. + */ +const BlockList: React.FC = ({ + footerComponent, + columns, + rowsPerPage, + tableSize, +}) => { + return ( + + ); +}; + +export default BlockList; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/BlockList/blockColumnsConfig.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/BlockList/blockColumnsConfig.ts new file mode 100644 index 0000000000..32e8aca638 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/BlockList/blockColumnsConfig.ts @@ -0,0 +1,25 @@ +/** + * Component user can select columns to be rendered in a table list. + * Possible fields and their configurations are defined in here. + */ +export const blockColumnsConfig = { + hash: { + name: "Hash", + field: "hash", + isLongString: true, + isUnique: true, + }, + number: { + name: "Number", + field: "number", + }, + createdAt: { + name: "Created At", + field: "created_at", + isDate: true, + }, + txCount: { + name: "Transaction Count", + field: "number_of_tx", + }, +}; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TokenHeader/TokenHeader.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TokenHeader/TokenHeader.tsx index 62c154cdfa..c7df8416cb 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TokenHeader/TokenHeader.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TokenHeader/TokenHeader.tsx @@ -27,7 +27,7 @@ function TokenHeader(props: { accountNum: string; tokenAddress: string }) {

Total supply: - {(data as TokenMetadata20).total_supply} + {(data as TokenMetadata20)?.total_supply}

diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TransactionList/TransactionList.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TransactionList/TransactionList.tsx new file mode 100644 index 0000000000..9bdb9f51c1 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TransactionList/TransactionList.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import { ethereumAllTransactionsQuery } from "../../queries"; +import { transactionColumnsConfig } from "./transactionColumnsConfig"; +import type { UITableListingPaginationActionProps } from "../../../../components/ui/UITableListing/UITableListingPaginationAction"; +import UITableListing from "../../../../components/ui/UITableListing/UITableListing"; + +/** + * List of columns that can be rendered in a transaction list table + */ +export type TransactionListColumn = keyof typeof transactionColumnsConfig; + +/** + * TransactionList properties. + */ +export interface TransactionListProps { + footerComponent: React.ComponentType; + columns: TransactionListColumn[]; + rowsPerPage: number; + tableSize?: "small" | "medium"; +} + +/** + * TransactionList - Show table with ethereum transactions. + * + * @param footerComponent component will be rendered in a footer of a transaction list table. + * @param columns list of columns to be rendered. + * @param rowsPerPage how many rows to show per page. + */ +const TransactionList: React.FC = ({ + footerComponent, + columns, + rowsPerPage, + tableSize, +}) => { + return ( + + ); +}; + +export default TransactionList; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TransactionList/transactionColumnsConfig.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TransactionList/transactionColumnsConfig.ts new file mode 100644 index 0000000000..a37590a6b9 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TransactionList/transactionColumnsConfig.ts @@ -0,0 +1,34 @@ +/** + * Component user can select columns to be rendered in transaction list. + * Possible fields and their configurations are defined in transactionColumnsConfig. + */ +export const transactionColumnsConfig = { + hash: { + name: "Hash", + field: "hash", + isLongString: true, + isUnique: true, + }, + block: { + name: "Block", + field: "block_number", + }, + from: { + name: "From", + field: "from", + isLongString: true, + }, + to: { + name: "To", + field: "to", + isLongString: true, + }, + value: { + name: "Value", + field: "eth_value", + }, + method: { + name: "Method", + field: "method_name", + }, +}; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx index 7a61779bcb..6eec88fbb7 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx @@ -50,16 +50,6 @@ const ethConfig: AppConfig = { }, ], }, - { - path: "block-details", - element: , - children: [ - { - path: ":number", - element: , - }, - ], - }, { path: "token-details", element: , @@ -74,16 +64,6 @@ const ethConfig: AppConfig = { }, ], }, - { - path: "txn-details", - element: , - children: [ - { - path: ":id", - element: , - }, - ], - }, { path: "erc20", element: , diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.module.css b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.module.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.tsx index 971cd4b14d..3f225c7425 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.tsx @@ -1,44 +1,18 @@ -import { useNavigate } from "react-router-dom"; -import CardWrapper from "../../../../components/ui/CardWrapper"; - -import styles from "./Blocks.module.css"; -import { useQuery } from "@tanstack/react-query"; -import { ethereumAllBlocksQuery } from "../../queries"; - -type ObjectKey = keyof typeof styles; - -function Blocks() { - const navigate = useNavigate(); - const { isError, data, error } = useQuery(ethereumAllBlocksQuery()); - - if (isError) { - console.error("Transactions fetch error:", error); - } - - const blocksTableProps = { - onClick: { - action: (param: string) => navigate(`/eth/block-details/${param}`), - prop: "number", - }, - schema: [ - { display: "created at", objProp: ["created_at"] }, - { display: "block number", objProp: ["number"] }, - { display: "hash", objProp: ["hash"] }, - ], - }; +import Box from "@mui/material/Box"; +import PageTitleWithGoBack from "../../../../components/ui/PageTitleWithGoBack"; +import BlockList from "../../components/BlockList/BlockList"; +import TablePaginationAction from "../../../../components/ui/UITableListing/UITableListingPaginationAction"; +export default function Blocks() { return ( -
- -
+ + Blocks + + ); } - -export default Blocks; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/BlockSummary.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/BlockSummary.tsx new file mode 100644 index 0000000000..e2d420dc0a --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/BlockSummary.tsx @@ -0,0 +1,31 @@ +import { Link as RouterLink } from "react-router-dom"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import BlockList from "../../components/BlockList/BlockList"; + +function BlockListViewAllAction() { + return ( + + + + + ); +} + +export default function BlockSummary() { + return ( + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/Dashboard.module.css b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/Dashboard.module.css deleted file mode 100644 index f2443fc562..0000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/Dashboard.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.dashboard-wrapper { - display: flex; - gap: 1rem; -} - -@media (max-width: 1699px) { - .dashboard-wrapper { - flex-direction: column; - } -} \ No newline at end of file diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/Dashboard.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/Dashboard.tsx index 8ebd00a314..fe5aa3a080 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/Dashboard.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/Dashboard.tsx @@ -1,16 +1,56 @@ -import styles from "./Dashboard.module.css"; -import Transactions from "../Transactions/Transactions"; -import Blocks from "../Blocks/Blocks"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; +import { SvgIconComponent } from "@mui/icons-material"; +import ReceiptOutlinedIcon from "@mui/icons-material/ReceiptOutlined"; +import HubIcon from "@mui/icons-material/Hub"; + +import PageTitle from "../../../../components/ui/PageTitle"; +import TransactionSummary from "./TransactionSummary"; +import BlockSummary from "./BlockSummary"; + +interface TitleWithIconProps { + icon: SvgIconComponent; + children: React.ReactNode; +} + +const TitleWithIcon: React.FC = ({ + children, + icon: Icon, +}) => { + return ( + + + + {children} + + + ); +}; function Dashboard() { return ( -
-

Dashboard

-
- - -
-
+ + Dashboard + } + justifyContent="space-between" + alignItems="center" + > + + Blocks + + + + + Transactions + + + + ); } diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/TransactionSummary.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/TransactionSummary.tsx new file mode 100644 index 0000000000..9b923b9623 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Dashboard/TransactionSummary.tsx @@ -0,0 +1,31 @@ +import { Link as RouterLink } from "react-router-dom"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import TransactionList from "../../components/TransactionList/TransactionList"; + +function TransactionListViewAllAction() { + return ( + + + + + ); +} + +export default function TransactionSummary() { + return ( + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.module.css b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.module.css deleted file mode 100644 index 7ff183b0da..0000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.transactions{ - display: flex; - flex-direction: column; - -}.transactions-search { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 2rem; - - -} \ No newline at end of file diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.tsx index 38716f9439..36f12a337e 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.tsx @@ -1,51 +1,18 @@ -import CardWrapper from "../../../../components/ui/CardWrapper"; - -import styles from "./Transactions.module.css"; -import { useNavigate } from "react-router-dom"; -import { useQuery } from "@tanstack/react-query"; -import { ethereumAllTransactionsQuery } from "../../queries"; - -function Transactions() { - const navigate = useNavigate(); - const { isError, data, error } = useQuery(ethereumAllTransactionsQuery()); - - if (isError) { - console.error("Transactions fetch error:", error); - } - - const txnTableProps = { - onClick: { - action: (param: string) => navigate(`/eth/txn-details/${param}`), - prop: "id", - }, - schema: [ - { - display: "transaction id", - objProp: ["id"], - }, - { - display: "sender/recipient", - objProp: ["from", "to"], - }, - { - display: "token value", - objProp: ["eth_value"], - }, - ], - }; +import Box from "@mui/material/Box"; +import TransactionList from "../../components/TransactionList/TransactionList"; +import PageTitleWithGoBack from "../../../../components/ui/PageTitleWithGoBack"; +import UITableListingPaginationAction from "../../../../components/ui/UITableListing/UITableListingPaginationAction"; +export default function Transactions() { return ( -
- -
+ + Transactions + + ); } - -export default Transactions; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts index 061fabf102..ba1f33a409 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts @@ -17,12 +17,60 @@ import { TokenMetadata20, } from "../../common/supabase-types"; -export function ethereumAllTransactionsQuery() { - return supabaseQueryTable("transaction"); +function createQueryKey( + tableName: string, + pagination: { page: number; pageSize: number }, +) { + return [tableName, { pagination }]; } -export function ethereumAllBlocksQuery() { - return supabaseQueryTable("block"); +export function ethereumAllTransactionsQuery(page: number, pageSize: number) { + const fromIndex = page * pageSize; + const toIndex = fromIndex + pageSize - 1; + const tableName = "transaction"; + return queryOptions({ + queryKey: [supabaseQueryKey, createQueryKey(tableName, { page, pageSize })], + queryFn: async () => { + const { data, error } = await supabase + .from(tableName) + .select() + .order("block_number", { ascending: false }) + .range(fromIndex, toIndex); + + if (error) { + throw new Error( + `Could not get data from '${tableName}' table: ${error.message}`, + ); + } + + return data as Transaction[]; + }, + }); +} + +// todo - refactor to single get-all query with paging +export function ethereumAllBlocksQuery(page: number, pageSize: number) { + const fromIndex = page * pageSize; + const toIndex = fromIndex + pageSize - 1; + const tableName = "block"; + return queryOptions({ + queryKey: [supabaseQueryKey, createQueryKey(tableName, { page, pageSize })], + queryFn: async () => { + const { data, error } = await supabase + .from(tableName) + .select() + .order("number", { ascending: false }) + .range(fromIndex, toIndex); + + if (error) { + throw new Error( + `Could not get data from '${tableName}' table: ${error.message}`, + ); + } + + return data as Block[]; + }, + }); } export function ethereumBlockByNumber(blockNumber: number | string) { diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/context/NotificationContext.tsx b/packages/cacti-ledger-browser/src/main/typescript/common/context/NotificationContext.tsx new file mode 100644 index 0000000000..dfc7d6332f --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/common/context/NotificationContext.tsx @@ -0,0 +1,72 @@ +import React, { createContext, useState, useContext, ReactNode } from "react"; +import Alert, { AlertColor } from "@mui/material/Alert"; +import Slide, { SlideProps } from "@mui/material/Slide"; +import Snackbar from "@mui/material/Snackbar"; + +const autoHideDuration = 1000 * 3; // 3 seconds +const defaultSeverity: AlertColor = "info"; + +type Notification = { + message: string; + severity?: AlertColor; +}; + +type NotificationContextType = { + showNotification: (message: string, severity?: AlertColor) => void; +}; + +const NotificationContext = createContext({ + showNotification: (message: string) => { + console.log("Notification before context init:", message); + }, +}); + +function SlideTransition(props: SlideProps) { + return ; +} + +export const NotificationProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [notification, setNotification] = useState( + undefined, + ); + const isNotification = Boolean(notification); + + const showNotification = (message: string, severity?: AlertColor) => { + if (severity === "error") { + console.error("Error notification:", message); + } + setNotification({ message, severity }); + }; + + const closeNotification = () => { + setNotification(undefined); + }; + + return ( + + {children} + {isNotification && ( + + + {notification?.message} + + + )} + + ); +}; + +export const useNotification = (): NotificationContextType => + useContext(NotificationContext); diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/CustomTable.module.css b/packages/cacti-ledger-browser/src/main/typescript/components/ui/CustomTable.module.css index 306f13ac57..888d6db07b 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/components/ui/CustomTable.module.css +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/CustomTable.module.css @@ -1,28 +1,28 @@ -table { +.custom-table { border-collapse: separate; border-spacing: 0; width: 100%; } -tbody tr { +.custom-table tbody tr { background-color: rgb(248, 248, 248); border: 1px solid rgb(219, 241, 232); border-radius: 10px; } -tbody tr:hover { +.custom-table tbody tr:hover { cursor: pointer; background-color: rgb(235, 240, 237); } -th { +.custom-table th { background-color: rgb(240, 235, 235); border-style: none; border-bottom: solid 1px rgb(155, 153, 153); padding: 10px; } -td { +.custom-table td { min-height: 2rem; border-style: none; border-bottom: solid 2px rgb(255, 255, 255); @@ -30,25 +30,25 @@ td { text-align: center; } -tr { +.custom-table tr { min-height: 20rem; background-color: rgb(90, 103, 116); padding: 1rem; } -tr:first-child th:first-child { +.custom-table tr:first-child th:first-child { border-top-left-radius: 10px; } -tr:first-child th:last-child { +.custom-table tr:first-child th:last-child { border-top-right-radius: 10px; } -tr:last-child td:first-child { +.custom-table tr:last-child td:first-child { border-bottom-left-radius: 10px; } -tr:last-child td:last-child { +.custom-table tr:last-child td:last-child { border-bottom-right-radius: 10px; } diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/CustomTable.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ui/CustomTable.tsx index 52c59e8121..e3feb54943 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/components/ui/CustomTable.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/CustomTable.tsx @@ -1,11 +1,15 @@ - - import EmptyTablePlaceholder from "./EmptyTablePlaceholder/EmptyTablePlaceholder"; import styles from "./CustomTable.module.css"; -import { useState, useEffect, ReactElement, JSXElementConstructor, ReactNode, ReactPortal } from "react"; +import { + useState, + useEffect, + ReactElement, + JSXElementConstructor, + ReactNode, + ReactPortal, +} from "react"; import { TableProperty } from "../../common/supabase-types"; - function CustomTable(props: any) { const [viewport, setViewport] = useState(""); @@ -42,7 +46,7 @@ function CustomTable(props: any) { ) : ( <> {viewport === "wide" && ( - +
{props.cols.schema.map((col: any) => ( @@ -71,25 +75,43 @@ function CustomTable(props: any) { {props.data.map((row: any) => { return (
handleRowClick(row)} > - {props.cols.schema.map((heading: { display: string | number | boolean | ReactElement> | Iterable | ReactPortal | null | undefined; }, idx: string | number) => { - return ( - - - - - ); - })} + {props.cols.schema.map( + ( + heading: { + display: + | string + | number + | boolean + | ReactElement< + any, + string | JSXElementConstructor + > + | Iterable + | ReactPortal + | null + | undefined; + }, + idx: string | number, + ) => { + return ( + + + + + ); + }, + )}
- {heading.display} - - {getObjPropVal( - props.cols.schema[idx].objProp, - row, - )} -
+ {heading.display} + + {getObjPropVal( + props.cols.schema[idx].objProp, + row, + )} +
); diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/PageTitle.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ui/PageTitle.tsx new file mode 100644 index 0000000000..747ea86a9e --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/PageTitle.tsx @@ -0,0 +1,21 @@ +import Typography from "@mui/material/Typography"; +import * as React from "react"; + +export interface PageTitleProps { + children: React.ReactNode; +} + +const PageTitle: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export default PageTitle; diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/PageTitleWithGoBack.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ui/PageTitleWithGoBack.tsx new file mode 100644 index 0000000000..d71ef0efc2 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/PageTitleWithGoBack.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { useNavigate } from "react-router-dom"; +import PageTitle from "./PageTitle"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; + +export interface PageTitleWithGoBackProps { + children: React.ReactNode; +} + +const PageTitleWithGoBack: React.FC = ({ + children, +}) => { + const navigate = useNavigate(); + + return ( + + navigate(-1)} + sx={{ marginBottom: 3 }} + > + + + {children} + + ); +}; + +export default PageTitleWithGoBack; diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/ShortHash.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ui/ShortHash.tsx new file mode 100644 index 0000000000..72d75c853a --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/ShortHash.tsx @@ -0,0 +1,34 @@ +import Tooltip from "@mui/material/Tooltip"; +import Typography, { TypographyProps } from "@mui/material/Typography"; + +const defaultMaxHashLength = 14; + +type ShortHashProps = { + hash: string; + maxLength?: number; +} & TypographyProps; + +/** + * Wrapper around MUI Typography for displaying shortified hash when necessary. + * Full hash will be shown in tooltip on hover. + * @param hash hash to be displayed. + * @param maxLength? maximum hash length (defualt is 14 + * @param TypographyProps? any additional props will be passed as Typography props + * + * @returns Short hash Typography with tooltip + */ +export default function ShortHash(params: ShortHashProps) { + const { hash, maxLength: inputMaxLength, ...typographyParams } = params; + const maxLength = inputMaxLength ? inputMaxLength : defaultMaxHashLength; + + if (hash.length <= maxLength) { + return {hash}; + } + + const shortHash = `...${hash.slice(-maxLength)}`; + return ( + + {shortHash} + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/StyledTableCell.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/StyledTableCell.tsx new file mode 100644 index 0000000000..244fe8e6f4 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/StyledTableCell.tsx @@ -0,0 +1,16 @@ +import TableCell from "@mui/material/TableCell"; +import { styled } from "@mui/material/styles"; + +export const tableCellHeight = 39; + +export const StyledTableCellHeader = styled(TableCell)(({ theme }) => ({ + fontSize: 17, + fontWeight: "bold", + color: theme.palette.primary.main, + height: tableCellHeight, + borderColor: theme.palette.primary.main, +})); + +export const StyledTableCell = styled(TableCell)(() => ({ + height: tableCellHeight, +})); diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/UITableListing.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/UITableListing.tsx new file mode 100644 index 0000000000..0c6fdaba15 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/UITableListing.tsx @@ -0,0 +1,190 @@ +import * as React from "react"; +import { + UseQueryOptions, + keepPreviousData, + useQuery, +} from "@tanstack/react-query"; +import Box from "@mui/material/Box"; +import Paper from "@mui/material/Paper"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableContainer from "@mui/material/TableContainer"; +import TableRow from "@mui/material/TableRow"; +import TableHead from "@mui/material/TableHead"; +import CircularProgress from "@mui/material/CircularProgress"; + +import { useNotification } from "../../../common/context/NotificationContext"; +import ShortHash from "../ShortHash"; +import { + StyledTableCell, + StyledTableCellHeader, + tableCellHeight, +} from "./StyledTableCell"; +import type { UITableListingPaginationActionProps } from "./UITableListingPaginationAction"; + +/** + * Table column configuration entry + * + * @param name Name of the column to be displayed in the header. + * @param field Name of the field in data returned from react-query + * @param isLongString? boolean if value is hash or not (i.e. should be shortened when necessary) + * @param isDate? boolean if value is a date (should be formatted differently) + * @param isUnique? boolean if value is unique (can be used as key) + */ +export type ColumnConfigEntry = { + name: string; + field: string; + isLongString?: boolean; + isDate?: boolean; + isUnique?: boolean; +}; + +export type ColumnConfigType = { [key: string]: ColumnConfigEntry }; + +/** + * UITableListing parameters + */ +export interface UITableListingProps { + queryFunction: ( + page: number, + pageSize: number, + ) => UseQueryOptions; + label: string; + columnConfig: ColumnConfigType; + footerComponent: React.ComponentType; + columns: string[]; + rowsPerPage: number; + tableSize?: "small" | "medium"; +} + +function getKeyField(columnConfig: ColumnConfigType) { + const keyField = Object.entries(columnConfig) + .find(([, config]) => config.isUnique) + ?.map((entry) => (entry as ColumnConfigEntry).field) + ?.pop(); + + if (!keyField) { + throw new Error( + `Could not find unique field to display UITableListing. Config: ${columnConfig}`, + ); + } + + return keyField; +} + +function formatCellValue(config: ColumnConfigEntry, value: any) { + if (config.isLongString) { + return ; + } else if (config.isDate) { + const date = new Date(value); + return date.toLocaleString(); + } + // Return plain value by default + return value; +} + +/** + * UITableListing - Show table with paged data fetched from react-query. + * + * Use higher level component when possible. + * Supports paging and error handling. Will show empty entries if number of entries + * is smaller then requested `rowsPerPage` to keep UI in place. + * + * @param footerComponent component will be rendered in a footer of a transaction list table. + * @param columns list of columns to be rendered. + * @param rowsPerPage how many rows to show per page. + */ +export default function UITableListing< + T extends { + [key: string]: any; + }, +>({ + queryFunction, + label, + columnConfig, + footerComponent: FooterComponent, + columns, + rowsPerPage, + tableSize, +}: UITableListingProps) { + const [page, setPage] = React.useState(0); + const { isError, isPending, data, error, refetch } = useQuery({ + ...queryFunction(page, rowsPerPage), + placeholderData: keepPreviousData, + }); + const { showNotification } = useNotification(); + const displayData: T[] = data ?? []; + + React.useEffect(() => { + isError && showNotification(`Could not fetch data: ${error}`, "error"); + }, [isError]); + + // Avoid a layout jump when reaching the last page with empty rows. + const emptyRows = Math.max(0, rowsPerPage - displayData.length); + + return ( + + {isPending && ( + + )} + + + + + {columns.map((colName) => { + return ( + + {columnConfig[colName].name} + + ); + })} + + + + {displayData.map((row) => ( + + {columns.map((colName) => { + const config = columnConfig[colName]; + const value = row[config.field]; + return ( + + {formatCellValue(config, value)} + + ); + })} + + ))} + {emptyRows > 0 && ( + + + + )} + +
+ { + setPage(newPage); + }} + onPageRefresh={() => { + refetch(); + }} + disableNext={emptyRows > 0} + /> +
+
+ ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/UITableListingPaginationAction.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/UITableListingPaginationAction.tsx new file mode 100644 index 0000000000..0991da732c --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/UITableListingPaginationAction.tsx @@ -0,0 +1,71 @@ +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import FirstPageIcon from "@mui/icons-material/FirstPage"; +import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; +import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; + +const actionButtonFontSize = 30; + +/** + * Pagination footer component interface. + */ +export interface UITableListingPaginationActionProps { + page: number; + disableNext: boolean; + onPageChange: (newPage: number) => void; + onPageRefresh: () => void; +} + +/** + * Pagination footer component to be used with `UITableListing`. + */ +const UITableListingPaginationAction: React.FC< + UITableListingPaginationActionProps +> = ({ page, disableNext, onPageChange, onPageRefresh }) => { + const homeButton = + page === 0 ? ( + + + + ) : ( + { + onPageChange(0); + }} + aria-label="first page" + > + + + ); + + return ( + + {homeButton} + + { + onPageChange(page - 1); + }} + disabled={page === 0} + aria-label="previous page" + > + + + { + onPageChange(page + 1); + }} + aria-label="next page" + disabled={disableNext} + > + + + + ); +}; + +export default UITableListingPaginationAction; diff --git a/packages/cacti-ledger-browser/src/main/typescript/theme.ts b/packages/cacti-ledger-browser/src/main/typescript/theme.ts index fde8309bbc..3c8273ac2b 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/theme.ts +++ b/packages/cacti-ledger-browser/src/main/typescript/theme.ts @@ -7,7 +7,7 @@ export const themeOptions: ThemeOptions = { main: "#2C6E49", }, secondary: { - main: "#e57373", + main: "#5D4037", }, warning: { main: "#D68C45",