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
+ }
+ 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 (
-
-
- {heading.display}
- |
-
- {getObjPropVal(
- props.cols.schema[idx].objProp,
- row,
- )}
- |
-
- );
- })}
+ {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,
+ )}
+ |
+
+ );
+ },
+ )}
);
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",