From 5dd905e113d5bb4ce95bf892b200126cdc652ad2 Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Fri, 19 Jul 2024 11:56:39 +0200 Subject: [PATCH] feat(ledger-browser): rewrite fabric application - Rewrite fabric app using MUI components and new database schema. - Improve `UITableListing` to support clickable tables. - The new app supports the following views: - Dashboard: Shows summary of blocks and transaction recorded in database. - Block list: Full list of blocks - Transaction list: Full list of transactions - Transaction details: Page that shows full transaction information, transaction actions (method calls) and endorsements. Depends on #3308 Depends on #3279 Signed-off-by: Michal Bajer --- packages/cacti-ledger-browser/package.json | 1 + .../apps/eth/pages/Dashboard/Dashboard.tsx | 22 +- .../fabric/components/BlockList/BlockList.tsx | 48 ++++ .../BlockList/blockColumnsConfig.ts | 20 ++ .../CertificateDetailsBox.tsx | 169 ++++++++++++ .../CertificateDialogButton.tsx | 73 +++++ .../components/TokenHeader/TokenAccount.tsx | 18 -- .../TokenHeader/TokenHeader.module.css | 57 ---- .../components/TokenHeader/TokenHeader.tsx | 52 ---- .../TransactionList/TransactionList.tsx | 60 +++++ .../transactionColumnsConfig.ts | 36 +++ .../fabric/components/ui/StackedRowItems.tsx | 23 ++ .../apps/fabric/fabric-supabase-types.ts | 57 ++++ .../src/main/typescript/apps/fabric/index.tsx | 39 ++- .../apps/fabric/pages/Blocks/Blocks.tsx | 18 ++ .../BlocksFabric/BlocksFabric.module.css | 0 .../pages/BlocksFabric/BlocksFabric.tsx | 62 ----- .../pages/DashFabric/DashFabric.module.css | 11 - .../fabric/pages/DashFabric/DashFabric.tsx | 103 ------- .../fabric/pages/Dashboard/BlockSummary.tsx | 31 +++ .../apps/fabric/pages/Dashboard/Dashboard.tsx | 37 +++ .../pages/Dashboard/TransactionSummary.tsx | 33 +++ .../pages/FabricBlock/FabricBlock.module.css | 8 - .../fabric/pages/FabricBlock/FabricBlock.tsx | 74 ----- .../FabricTransaction.module.css | 22 -- .../FabricTransaction/FabricTransaction.tsx | 138 ---------- .../TransactionDetails/TranactionInfoCard.tsx | 64 +++++ .../TransactionActionEndorsementsTable.tsx | 71 +++++ .../TransactionActionsTable.tsx | 254 ++++++++++++++++++ .../TransactionDetails/TransactionDetails.tsx | 73 +++++ .../pages/Transactions/Transactions.tsx | 20 ++ .../TransactionsFabric.module.css | 0 .../TransactionsFabric/TransactionsFabric.tsx | 59 ---- .../main/typescript/apps/fabric/queries.ts | 201 ++++++++++++++ .../components/ui/TitleWithIcon.tsx | 24 ++ .../ui/UITableListing/ClickableTableRow.tsx | 12 + .../ui/UITableListing/UITableListing.tsx | 50 +++- yarn.lock | 1 + 38 files changed, 1380 insertions(+), 661 deletions(-) create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/BlockList/BlockList.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/BlockList/blockColumnsConfig.ts create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDetailsBox.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDialogButton.tsx delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenAccount.tsx delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenHeader.module.css delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenHeader.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TransactionList/TransactionList.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TransactionList/transactionColumnsConfig.ts create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/ui/StackedRowItems.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/fabric-supabase-types.ts create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Blocks/Blocks.tsx delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/BlocksFabric/BlocksFabric.module.css delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/BlocksFabric/BlocksFabric.tsx delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/DashFabric/DashFabric.module.css delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/DashFabric/DashFabric.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/BlockSummary.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/Dashboard.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/TransactionSummary.tsx delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricBlock/FabricBlock.module.css delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricBlock/FabricBlock.tsx delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricTransaction/FabricTransaction.module.css delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricTransaction/FabricTransaction.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TranactionInfoCard.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionEndorsementsTable.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionsTable.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionDetails.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Transactions/Transactions.tsx delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionsFabric/TransactionsFabric.module.css delete mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionsFabric/TransactionsFabric.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/apps/fabric/queries.ts create mode 100644 packages/cacti-ledger-browser/src/main/typescript/components/ui/TitleWithIcon.tsx create mode 100644 packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/ClickableTableRow.tsx diff --git a/packages/cacti-ledger-browser/package.json b/packages/cacti-ledger-browser/package.json index 174556ac8d1..3088d0d3cbd 100644 --- a/packages/cacti-ledger-browser/package.json +++ b/packages/cacti-ledger-browser/package.json @@ -66,6 +66,7 @@ "@supabase/supabase-js": "1.35.6", "@tanstack/react-query": "5.29.2", "apexcharts": "3.45.2", + "buffer": "6.0.3", "ethers": "6.12.1", "react": "18.2.0", "react-apexcharts": "1.4.1", 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 fe5aa3a0805..58b3779806b 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,34 +1,14 @@ 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 TitleWithIcon from "../../../../components/ui/TitleWithIcon"; 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 ( diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/BlockList/BlockList.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/BlockList/BlockList.tsx new file mode 100644 index 00000000000..b3bfc8944a8 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/BlockList/BlockList.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import { fabricAllBlocksQuery } 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 fabric 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/fabric/components/BlockList/blockColumnsConfig.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/BlockList/blockColumnsConfig.ts new file mode 100644 index 00000000000..09d561fbdfc --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/BlockList/blockColumnsConfig.ts @@ -0,0 +1,20 @@ +/** + * Component user can select columns to be rendered in a table list. + * Possible fields and their configurations are defined in here. + */ +export const blockColumnsConfig = { + number: { + name: "Number", + field: "number", + }, + hash: { + name: "Hash", + field: "hash", + isLongString: true, + isUnique: true, + }, + txCount: { + name: "Transaction Count", + field: "transaction_count", + }, +}; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDetailsBox.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDetailsBox.tsx new file mode 100644 index 00000000000..df79eac865a --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDetailsBox.tsx @@ -0,0 +1,169 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import TextField from "@mui/material/TextField"; +import { styled } from "@mui/material/styles"; + +import { FabricCertificate } from "../../fabric-supabase-types"; +import StackedRowItems from "../ui/StackedRowItems"; + +const ListHeaderTypography = styled(Typography)(({ theme }) => ({ + color: theme.palette.secondary.main, + fontWeight: "bold", +})); + +const TextFieldDisabledBlackFont = styled(TextField)(() => ({ + "& .MuiInputBase-input.Mui-disabled": { + WebkitTextFillColor: "black", + }, +})); + +function formatDateString(date: string | undefined) { + if (date) { + return new Date(date).toLocaleDateString(); + } + + return "-"; +} + +function formatCertificateAttr(attr: string | null | undefined) { + if (attr) { + return attr; + } + + return "-"; +} + +function formatCertificateSubject( + certificate: FabricCertificate, + fieldPrefix: "issuer" | "subject", +) { + return ( +
    +
  • + + Common Name: + + {formatCertificateAttr(certificate[`${fieldPrefix}_common_name`])} + + +
  • +
  • + + Organization: + + {formatCertificateAttr(certificate[`${fieldPrefix}_org`])} + + +
  • +
  • + + Organization Unit: + + {formatCertificateAttr(certificate[`${fieldPrefix}_org_unit`])} + + +
  • +
  • + + Country: + + {formatCertificateAttr(certificate[`${fieldPrefix}_country`])} + + +
  • +
  • + + Locality: + + {formatCertificateAttr(certificate[`${fieldPrefix}_locality`])} + + +
  • +
  • + + State: + + {formatCertificateAttr(certificate[`${fieldPrefix}_state`])} + + +
  • +
+ ); +} + +export interface CertificateDetailsBoxProps { + certificate: FabricCertificate | undefined; +} + +/** + * Detailed information of provided fabric certificate. + * @param certificate: Fabric certificate from the DB. + */ +export default function CertificateDetailsBox({ + certificate, +}: CertificateDetailsBoxProps) { + return ( + + + Serial Number: + + {formatCertificateAttr(certificate?.serial_number)} + + + + Valid From: + + {formatCertificateAttr(formatDateString(certificate?.valid_from))} + + + + Valid To: + + {formatCertificateAttr(formatDateString(certificate?.valid_to))} + + + + Alt Name: + + {formatCertificateAttr(certificate?.subject_alt_name)} + + + + {certificate ? ( + <> + Subject: + {formatCertificateSubject(certificate, "subject")} + + ) : ( + + Subject: + - + + )} + + {certificate ? ( + <> + Issuer: + {formatCertificateSubject(certificate, "issuer")} + + ) : ( + + Issuer: + - + + )} + + + Certificate (PEM): + + + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDialogButton.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDialogButton.tsx new file mode 100644 index 00000000000..107cb668a9b --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDialogButton.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; +import { useQuery } from "@tanstack/react-query"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; + +import CertificateDetailsBox from "./CertificateDetailsBox"; +import { fabricCertificate } from "../../queries"; +import { useNotification } from "../../../../common/context/NotificationContext"; + +export interface CertificateDialogButtonProps { + certId: string | null; +} + +/** + * Show a button with certificate common name and organization, that will + * open a dialog with full certificate details on click. + * + * @warn Fetches the certificate from DB when mounted. + * + * @param certId ID of the certificate in database. + */ +export default function CertificateDialogButton({ + certId, +}: CertificateDialogButtonProps) { + const { showNotification } = useNotification(); + const [openDialog, setOpenDialog] = React.useState(false); + + const { isError, data, error } = useQuery({ + ...fabricCertificate(certId ?? ""), + enabled: !!certId, + }); + + React.useEffect(() => { + isError && + showNotification( + `Could not fetch creator certificate: ${error}`, + "error", + ); + }, [isError]); + + let creatorName = "unknownName"; + if (data?.subject_common_name) { + creatorName = data.subject_common_name; + } + + let creatorOrg = "unknownOrg"; + if (data?.subject_org) { + creatorOrg = data.subject_org; + } else if (data?.subject_org_unit) { + creatorOrg = data.subject_org_unit; + } + + return ( + <> + + setOpenDialog(false)} open={openDialog}> + Certificate Details + + + + + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenAccount.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenAccount.tsx deleted file mode 100644 index 565880406c4..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenAccount.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import styles from "./TokenHeader.module.css"; -import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet"; - -function TokenAccount(props: any) { - return ( -
- - {" "} - - - {" "} - {props.accountNum} - -
- ); -} - -export default TokenAccount; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenHeader.module.css b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenHeader.module.css deleted file mode 100644 index 2d9ef9600c8..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenHeader.module.css +++ /dev/null @@ -1,57 +0,0 @@ -.token-header { - display: flex; - flex-direction: column; - width: 100%; - gap: 1rem; -} - -.token-details { - width: 100%; - height: min-content; - border: 1px solid rgb(240, 236, 236); - border-radius: 10px; - gap: 3rem; - padding: 1rem 2rem; - display: flex; - justify-content: flex-start; - align-items: center; - background-color: rgb(247, 245, 245); -} - -.token-details div { - display: flex; - align-items: center; - gap: 1rem; -} - -.token-icon { - height: 100%; - transform: translateY(10%); - color: rgb(34, 70, 70); -} - -.token-account { - font-size: 16px; - width: 100%; - height: min-content; - display: flex; - align-items: center; - justify-content: center; - background-color: rgb(247, 245, 245); - border-radius: 10px; - padding: 1rem; - padding-left: 2rem; -} - -.token-account span { - display: flex; - align-items: center; - gap: .5rem; -} - -.token-account-icon { - color: rgb(22, 92, 65); - font-size: 28px; - height: 30px; - width: 30px; -} \ No newline at end of file diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenHeader.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenHeader.tsx deleted file mode 100644 index fa846452d1c..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TokenHeader/TokenHeader.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { supabase } from "../../../../common/supabase-client"; -import { TokenMetadata20 } from "../../../../common/supabase-types"; -import TokenAccount from "./TokenAccount"; - - -import styles from "./TokenHeader.module.css"; -import { useEffect, useState } from "react"; - -function TokenHeader(props: any) { - const [tokenData, setTokenData] = useState(); - - const fetchData = async () => { - try { - const { data } = await supabase - .from(`token_metadata_erc20`) - .select("*") - .match({ address: props.token_address }); - console.log(data); - if (data?.[0]) { - setTokenData(data[0]); - } else { - throw new Error("Failed to load token details"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchData(); - }, []); - - return ( -
- -
-

- Address: {props.token_address} -

-

- Created at: {tokenData?.created_at} -

-

- Total supply: - {tokenData?.total_supply} -

-
-
- ); -} - -export default TokenHeader; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TransactionList/TransactionList.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TransactionList/TransactionList.tsx new file mode 100644 index 00000000000..01d56ff6ed8 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TransactionList/TransactionList.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import { useNavigate } from "react-router-dom"; +import { UseQueryOptions } from "@tanstack/react-query"; +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 { + queryFunction: ( + page: number, + pageSize: number, + ) => UseQueryOptions; + footerComponent: React.ComponentType; + columns: TransactionListColumn[]; + rowsPerPage: number; + tableSize?: "small" | "medium"; +} + +/** + * TransactionList - Show table with fabric 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. + */ + +export default function TransactionList< + T extends { + [key: string]: any; + }, +>({ + queryFunction, + footerComponent, + columns, + rowsPerPage, + tableSize, +}: TransactionListProps) { + const navigate = useNavigate(); + + return ( + navigate(`../transaction/${row.hash}`)} + /> + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TransactionList/transactionColumnsConfig.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TransactionList/transactionColumnsConfig.ts new file mode 100644 index 00000000000..71d0d75f049 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/TransactionList/transactionColumnsConfig.ts @@ -0,0 +1,36 @@ +/** + * 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, + }, + timestamp: { + name: "Timestamp", + field: "timestamp", + }, + type: { + name: "Type", + field: "type", + }, + epoch: { + name: "Epoch", + field: "epoch", + }, + channel_id: { + name: "Channel ID", + field: "channel_id", + }, + protocol_version: { + name: "Proto Version", + field: "protocol_version", + }, + block: { + name: "Block", + field: "block_number", + }, +}; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/ui/StackedRowItems.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/ui/StackedRowItems.tsx new file mode 100644 index 00000000000..e22fab4fe4e --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/ui/StackedRowItems.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import Stack from "@mui/material/Stack"; + +export interface StackedRowItemsProps { + children: React.ReactElement[]; +} + +/** + * Simple component that puts all the children in a row, space-between with minimal spacing 1. + * Uses MUI Stack. + */ +export default function StackedRowItems({ children }: StackedRowItemsProps) { + return ( + + {children} + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/fabric-supabase-types.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/fabric-supabase-types.ts new file mode 100644 index 00000000000..4c16f694ff3 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/fabric-supabase-types.ts @@ -0,0 +1,57 @@ +export interface FabricBlock { + id: number; + number: number; + hash: string; + transaction_count: number; +} + +export interface FabricTransaction { + block_id: string | null; + block_number: number | null; + channel_id: string; + epoch: number; + hash: string; + id: string; + protocol_version: number; + timestamp: string; + type: string; +} + +export interface FabricTransactionAction { + chaincode_id: string; + creator_certificate_id: string | null; + creator_msp_id: string; + function_args: string | null; + function_name: string | null; + id: string; + transaction_id: string | null; +} + +export interface FabricTransactionActionEndorsement { + certificate_id: string; + id: string; + mspid: string; + signature: string; + transaction_action_id: string | null; +} + +export interface FabricCertificate { + id: string; + issuer_common_name: string | null; + issuer_country: string | null; + issuer_locality: string | null; + issuer_org: string | null; + issuer_org_unit: string | null; + issuer_state: string | null; + pem: string; + serial_number: string; + subject_alt_name: string; + subject_common_name: string | null; + subject_country: string | null; + subject_locality: string | null; + subject_org: string | null; + subject_org_unit: string | null; + subject_state: string | null; + valid_from: string; + valid_to: string; +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/index.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/index.tsx index 2cc2e4c1500..e534033c985 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/index.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/index.tsx @@ -1,10 +1,9 @@ -import DashFabric from "./pages/DashFabric/DashFabric"; -import TransactionsFabric from "./pages/TransactionsFabric/TransactionsFabric"; -import BlocksFabric from "./pages/BlocksFabric/BlocksFabric"; -import FabricTransaction from "./pages/FabricTransaction/FabricTransaction"; -import FabricBlock from "./pages/FabricBlock/FabricBlock"; -import { Outlet } from "react-router-dom"; import { AppConfig } from "../../common/types/app"; +import Dashboard from "./pages/Dashboard/Dashboard"; +import Blocks from "./pages/Blocks/Blocks"; +import Transactions from "./pages/Transactions/Transactions"; +import { Outlet } from "react-router-dom"; +import TransactionDetails from "./pages/TransactionDetails/TransactionDetails"; const fabricConfig: AppConfig = { name: "Fabric", @@ -17,33 +16,27 @@ const fabricConfig: AppConfig = { ], routes: [ { - element: , - }, - { - path: "transactions", - element: , + element: , }, { path: "blocks", - element: , + element: , }, { - path: "txn-details", - element: , - children: [ - { - path: ":id", - element: , - }, - ], + path: "transactions", + element: , }, { - path: "block-details", + path: "transaction", element: , children: [ { - path: ":id", - element: , + path: ":hash", + element: ( +
+ +
+ ), }, ], }, diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Blocks/Blocks.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Blocks/Blocks.tsx new file mode 100644 index 00000000000..ca33a573019 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Blocks/Blocks.tsx @@ -0,0 +1,18 @@ +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 + + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/BlocksFabric/BlocksFabric.module.css b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/BlocksFabric/BlocksFabric.module.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/BlocksFabric/BlocksFabric.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/BlocksFabric/BlocksFabric.tsx deleted file mode 100644 index 55a404ace42..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/BlocksFabric/BlocksFabric.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { supabase } from "../../../../common/supabase-client"; -import CardWrapper from "../../../../components/ui/CardWrapper"; -import { Block } from "../../../../common/supabase-types"; -import styles from "./BlocksFabric.module.css"; -import { useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; - -type ObjectKey = keyof typeof styles; - -const BlocksFabric = () => { - const navigate = useNavigate(); - const [block, setBlock] = useState([]); - - const blocksTableProps = { - onClick: { - action: (param: string) => { - navigate(`/fabric/block-details/${param}`); - }, - prop: "id", - }, - schema: [ - { display: "created at", objProp: ["created_at"] }, - { display: "block number", objProp: ["block_number"] }, - { display: "channel name", objProp: ["channel_id"] }, - { display: "hash", objProp: ["data_hash"] }, - { display: "transactions count", objProp: ["tx_count"] }, - ], - }; - - const fetchBlock = async () => { - try { - const { data, error } = await supabase.from("fabric_blocks").select("*"); - if (data) { - setBlock(data); - } - if (error) { - console.error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchBlock(); - }, []); - - return ( -
- -
- ); -}; - -export default BlocksFabric; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/DashFabric/DashFabric.module.css b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/DashFabric/DashFabric.module.css deleted file mode 100644 index f75a30b7670..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/DashFabric/DashFabric.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.dashboard-wrapper { - display: flex; - gap: 1rem; - flex-direction: column; -} - -@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/fabric/pages/DashFabric/DashFabric.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/DashFabric/DashFabric.tsx deleted file mode 100644 index dedc76e96f3..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/DashFabric/DashFabric.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { supabase } from "../../../../common/supabase-client"; -import CardWrapper from "../../../../components/ui/CardWrapper"; -import { Transaction } from "../../../../common/supabase-types"; -import { Block } from "../../../../common/supabase-types"; -import styles from "./DashFabric.module.css"; -import { useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; - -function DashFabric() { - const navigate = useNavigate(); - const [transaction, setTransaction] = useState([]); - const [block, setBlock] = useState([]); - - const txnTableProps = { - onClick: { - action: (param: string) => { - navigate(`/fabric/txn-details/${param}`); - }, - prop: "id", - }, - schema: [ - { display: "created at", objProp: ["created_at"] }, - { display: "transaction id", objProp: ["transaction_id"] }, - { display: "channel name", objProp: ["channel_id"] }, - { display: "block id", objProp: ["block_id"] }, - { display: "status", objProp: ["status"] }, - ], - }; - - const blocksTableProps = { - onClick: { - action: (param: string) => { - navigate(`/fabric/block-details/${param}`); - }, - prop: "id", - }, - schema: [ - { display: "created at", objProp: ["created_at"] }, - { display: "block number", objProp: ["block_number"] }, - { display: "channel name", objProp: ["channel_id"] }, - { display: "hash", objProp: ["data_hash"] }, - { display: "transactions count", objProp: ["tx_count"] }, - ], - }; - - const fetchTransactions = async () => { - try { - const { data, error } = await supabase - .from("fabric_transactions") - .select("*"); - if (data) { - setTransaction(data); - } - if (error) { - console.error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - const fetchBlock = async () => { - try { - const { data, error } = await supabase.from("fabric_blocks").select("*"); - if (data) { - setBlock(data); - } - if (error) { - console.error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchBlock(); - fetchTransactions(); - }, []); - - return ( -
-
- - -
-
- ); -} - -export default DashFabric; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/BlockSummary.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/BlockSummary.tsx new file mode 100644 index 00000000000..ccb5a7bba21 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/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/fabric/pages/Dashboard/Dashboard.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/Dashboard.tsx new file mode 100644 index 00000000000..58b3779806b --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/Dashboard.tsx @@ -0,0 +1,37 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Divider from "@mui/material/Divider"; +import ReceiptOutlinedIcon from "@mui/icons-material/ReceiptOutlined"; +import HubIcon from "@mui/icons-material/Hub"; + +import PageTitle from "../../../../components/ui/PageTitle"; +import TitleWithIcon from "../../../../components/ui/TitleWithIcon"; +import TransactionSummary from "./TransactionSummary"; +import BlockSummary from "./BlockSummary"; + +function Dashboard() { + return ( + + Dashboard + } + justifyContent="space-between" + alignItems="center" + > + + Blocks + + + + + Transactions + + + + + ); +} + +export default Dashboard; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/TransactionSummary.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/TransactionSummary.tsx new file mode 100644 index 00000000000..676e54185b0 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Dashboard/TransactionSummary.tsx @@ -0,0 +1,33 @@ +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"; +import { fabricAllTransactionsQuery } from "../../queries"; + +function TransactionListViewAllAction() { + return ( + + + + + ); +} + +export default function TransactionSummary() { + return ( + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricBlock/FabricBlock.module.css b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricBlock/FabricBlock.module.css deleted file mode 100644 index 5bfa3e269ee..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricBlock/FabricBlock.module.css +++ /dev/null @@ -1,8 +0,0 @@ -.details-card { - display: flex; - flex-direction: column; - gap: 15px; - border: 1px solid rgb(230, 224, 224); - border-radius: 10px; - padding: 1.5rem 2rem; -} \ No newline at end of file diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricBlock/FabricBlock.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricBlock/FabricBlock.tsx deleted file mode 100644 index 92327116314..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricBlock/FabricBlock.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import styles from "./FabricBlock.module.css"; - -import { supabase } from "../../../../common/supabase-client"; -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; - -const FabricBlock = () => { - const [details, setDetails] = useState({}); - const params = useParams(); - - const fetchData = async () => { - try { - const { data, error } = await supabase - .from("fabric_blocks") - .select("*") - .match({ id: params.id }); - if (data?.[0]) { - setDetails(data[0]); - } else { - throw new Error("Failed to load block details"); - } - } catch (error: any) { - console.error(error.message); - } - }; - useEffect(() => { - fetchData(); - }, []); - - return ( -
-
- {details ? ( - <> -

Block Details

-

- ID: {details.id}{" "} -

-

- {" "} - Block Number: - {details.block_number} -

-

- Hash: - {details.data_hash} -

-

- Tx Count: - {details.tx_count} -

-

- Created at: - {details.created_at} -

-

- {" "} - Previous Blockhash: - {details.prev_blockhash} -

-

- {" "} - Channel name: - {details.channel_id} -

- - ) : ( -
Failed to load details
- )} -
-
- ); -}; -export default FabricBlock; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricTransaction/FabricTransaction.module.css b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricTransaction/FabricTransaction.module.css deleted file mode 100644 index 15fab73ab7e..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricTransaction/FabricTransaction.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.details-card { - display: flex; - flex-direction: column; - gap: 15px; - border: 1px solid rgb(230, 224, 224); - border-radius: 10px; - padding: 1.5rem 2rem; -} - -.details-bytes-wrap { - display: flex; - align-items: center; - gap: 1rem; -} -.details-bytes { - width: 30vw; - font-size: 14px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - resize: horizontal; -} \ No newline at end of file diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricTransaction/FabricTransaction.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricTransaction/FabricTransaction.tsx deleted file mode 100644 index 8a749087c2d..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/FabricTransaction/FabricTransaction.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import styles from "./FabricTransaction.module.css"; -import { supabase } from "../../../../common/supabase-client"; -import Button from "../../../../components/ui/Button"; -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; - -const notify = () => - alert("Success! Creator ID Bytes was successfully copied to the clipboard."); -// toast("Success! Creator ID Bytes was successfully copied to the clipboard."); - -function FabricTransaction() { - const [details, setDetails] = useState({}); - const params = useParams(); - - const fetchData = async () => { - try { - const { data, error } = await supabase - .from("fabric_transactions") - .select("*") - .match({ id: params.id }); - if (data?.[0]) { - console.log(data); - setDetails(data[0]); - } else { - throw new Error("Failed to load block details"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchData(); - }, []); - - const copyIdToClipboard = () => { - navigator.clipboard.writeText(details.creator_id_bytes); - notify(); - }; - - return ( -
-
- {details ? ( - <> - <> -

Transaction Details

-

- Created at: - {details.created_at} -

-

- Block ID: {details.block_id}{" "} -

-

- {" "} - Transaction ID: - {details.transaction_id} -

-

- {" "} - Channel name: - {details.channel_id} -

-

- {" "} - Status - {details.status} -

-

- {" "} - Type - {details.type} -

- -

- {" "} - Chaincode Name: - {details.chaincode_name} -

-

- {" "} - Creator ID Bytes: - - {" "} - {details.creator_id_bytes} - - -

-

- {" "} - Creator nonce: - {details.creator_nonce} -

-

- {" "} - Creator MSP ID: - {details.creator_msp_id} -

-

- {" "} - Endorser MSP ID: - {details.endorser_msp_id} -

-

- {" "} - Payload Proposal Hash: - {details.payload_proposal_hash} -

- - ) : ( -
Failed to load details
- )} -
- {/* */} -
- ); -} -export default FabricTransaction; - -{ - /* -

- {" "} - Validation Code - {details.validation_code} -

-

- {" "} - Network name: - {details.network_name} -

- */ -} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TranactionInfoCard.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TranactionInfoCard.tsx new file mode 100644 index 00000000000..055874a6986 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TranactionInfoCard.tsx @@ -0,0 +1,64 @@ +import { styled } from "@mui/material/styles"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Paper from "@mui/material/Paper"; +import Skeleton from "@mui/material/Skeleton"; + +import { FabricTransaction } from "../../fabric-supabase-types"; +import ShortenedTypography from "../../../../components/ui/ShortenedTypography"; +import StackedRowItems from "../../components/ui/StackedRowItems"; + +const ListHeaderTypography = styled(Typography)(({ theme }) => ({ + color: theme.palette.secondary.main, + fontWeight: "bold", +})); + +export interface TranactionInfoCardProps { + tx?: FabricTransaction; +} + +/** + * Card with basic transaction information. + * + * @param tx transaction object from a database + */ +export default function TranactionInfoCard({ tx }: TranactionInfoCardProps) { + if (!tx) { + return ; + } + + return ( + + + + Common Name: + + + + Timestamp: + {new Date(tx.timestamp).toLocaleString()} + + + Block Number: + {tx.block_number ?? "unknown"} + + + Channel ID: + {tx.channel_id} + + + Type: + {tx.type} + + + Epoch: + {tx.epoch} + + + Protocol Version: + {tx.protocol_version} + + + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionEndorsementsTable.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionEndorsementsTable.tsx new file mode 100644 index 00000000000..afe4d5b9c49 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionEndorsementsTable.tsx @@ -0,0 +1,71 @@ +import * as React from "react"; +import { useQuery } from "@tanstack/react-query"; +import Box from "@mui/material/Box"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; + +import { fabricActionEndorsements } from "../../queries"; +import { useNotification } from "../../../../common/context/NotificationContext"; +import ShortenedTypography from "../../../../components/ui/ShortenedTypography"; +import CertificateDialogButton from "../../components/CertificateDetails/CertificateDialogButton"; + +export interface TransactionActionEndorsementsTableProps { + actionId: string; +} + +/** + * Table of transaction action endorsements. + * + * @warn Fetches the endorsements from a database - don't mount when not needed. + * + * @param actionId ID of an action in a database + */ +export default function TransactionActionEndorsementsTable({ + actionId, +}: TransactionActionEndorsementsTableProps) { + const { showNotification } = useNotification(); + + const { isError, data, error } = useQuery({ + ...fabricActionEndorsements(actionId), + }); + const displayData = data ?? []; + + React.useEffect(() => { + isError && + showNotification( + `Could not fetch action endorsements: ${error}`, + "error", + ); + }, [isError]); + + return ( + + + + + Signature + Msp ID + Identity + + + + {displayData.map((row) => ( + + + + + {row.mspid} + + + + + ))} + +
+
+ ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionsTable.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionsTable.tsx new file mode 100644 index 00000000000..947e3ed890b --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionsTable.tsx @@ -0,0 +1,254 @@ +import * as React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Buffer } from "buffer"; +import { styled, useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Collapse from "@mui/material/Collapse"; +import IconButton from "@mui/material/IconButton"; +import Table from "@mui/material/Table"; +import Stack from "@mui/material/Stack"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import Typography from "@mui/material/Typography"; +import Paper from "@mui/material/Paper"; +import TextField from "@mui/material/TextField"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; + +import { fabricTransactionActions } from "../../queries"; +import { FabricTransactionAction } from "../../fabric-supabase-types"; +import { + StyledTableCellHeader, + StyledTableCell, +} from "../../../../components/ui/UITableListing/StyledTableCell"; +import CertificateDialogButton from "../../components/CertificateDetails/CertificateDialogButton"; +import TransactionActionEndorsementsTable from "./TransactionActionEndorsementsTable"; +import { useNotification } from "../../../../common/context/NotificationContext"; + +const TextFieldDisabledBlackFont = styled(TextField)(() => ({ + "& .MuiInputBase-input.Mui-disabled": { + WebkitTextFillColor: "black", + }, +})); + +interface ExpandableActionRowProps { + row: FabricTransactionAction; +} + +/** + * Single action row that can be expanded to show more details. + * + * @param row action row from a database + */ +function ExpandableActionRow({ row }: ExpandableActionRowProps) { + const theme = useTheme(); + const [open, setOpen] = React.useState(false); + const [showFunctionArgs, setShowFunctionArgs] = React.useState(true); + const [showActionCreator, setShowActionCreator] = React.useState(false); + const [showActionEndorsements, setShowActionEndorsements] = + React.useState(false); + + const functionArgs = (row.function_args ?? "").split(",").filter((a) => !!a); + const decodedFunctionArgs = functionArgs.map((a) => + Buffer.from(a.substring(2), "hex").toString("utf-8"), + ); + + return ( + + {/* Basic row entry */} + *": { borderBottom: "unset" }, + }} + > + + {row.function_name} + + + {decodedFunctionArgs.length} + + {row.chaincode_id} + {row.creator_msp_id} + + setOpen(!open)} + > + {open ? : } + + + + + {/* Expanded row details */} + + {/* Green strip on the left */} + + + + {/* Function Args */} + + + + Function Args + + setShowFunctionArgs(!showFunctionArgs)} + > + {showFunctionArgs ? ( + + ) : ( + + )} + + + {showFunctionArgs ? ( +
    + {decodedFunctionArgs.map((a) => ( +
  1. + +
  2. + ))} +
+ ) : undefined} +
+ + {/* Creator */} + + + + Creator ({row.creator_msp_id}) + + setShowActionCreator(!showActionCreator)} + > + {showActionCreator ? ( + + ) : ( + + )} + + + {showActionCreator ? ( + + ) : undefined} + + + {/* Endorsements */} + + + + Endorsements + + + setShowActionEndorsements(!showActionEndorsements) + } + > + {showActionEndorsements ? ( + + ) : ( + + )} + + + {showActionEndorsements ? ( + + ) : undefined} + +
+
+
+
+
+ ); +} + +export interface TransactionActionsTableProps { + txId?: string; +} + +/** + * Table with actions of the specified transaction. + * Fetches the needed data from a database. + * + * @param txId transaction id in the database (not hash!) + */ +export default function TransactionActionsTable({ + txId, +}: TransactionActionsTableProps) { + const { showNotification } = useNotification(); + + const { isError, data, error } = useQuery({ + ...fabricTransactionActions(txId ?? ""), + enabled: !!txId, + }); + const displayData = data ?? []; + + React.useEffect(() => { + isError && + showNotification( + `Could not fetch transaction actions: ${error}`, + "error", + ); + }, [isError]); + + return ( + + + + + Function Name + + Args Count + + + Chaincode ID + + + Creator MSP ID + + + + + + {displayData.map((row) => ( + + ))} + +
+
+ ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionDetails.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionDetails.tsx new file mode 100644 index 00000000000..3bc0edc8d35 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionDetails.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useNavigate, useParams } from "react-router-dom"; +import CircularProgress from "@mui/material/CircularProgress"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +import TranactionInfoCard from "./TranactionInfoCard"; +import { fabricTransactionByHash } from "../../queries"; +import { useNotification } from "../../../../common/context/NotificationContext"; +import PageTitleWithGoBack from "../../../../components/ui/PageTitleWithGoBack"; +import TransactionActionsTable from "./TransactionActionsTable"; + +/** + * Transaction details page, must be called with valid `hash` in a param. + * Fetched transaction details from a database. + */ +export default function TransactionDetails() { + const { showNotification } = useNotification(); + const navigate = useNavigate(); + const { hash } = useParams(); + if (!hash) { + showNotification(`Invalid transaction hash provided: ${hash}`, "error"); + navigate(".."); // Go to home + return null; + } + + const { isError, isPending, data, error } = useQuery( + fabricTransactionByHash(hash), + ); + + React.useEffect(() => { + isError && + showNotification( + `Could not fetch transaction details: ${error}`, + "error", + ); + }, [isError]); + + return ( + + Transaction Details + {isPending && ( + + )} + + + + + + + Actions + + + + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Transactions/Transactions.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Transactions/Transactions.tsx new file mode 100644 index 00000000000..1ffb31953d4 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/Transactions/Transactions.tsx @@ -0,0 +1,20 @@ +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"; +import { fabricAllTransactionsQuery } from "../../queries"; + +export default function Transactions() { + return ( + + Transactions + + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionsFabric/TransactionsFabric.module.css b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionsFabric/TransactionsFabric.module.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionsFabric/TransactionsFabric.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionsFabric/TransactionsFabric.tsx deleted file mode 100644 index 234355fddcb..00000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionsFabric/TransactionsFabric.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { supabase } from "../../../../common/supabase-client"; -import { Transaction } from "../../../../common/supabase-types"; -import CardWrapper from "../../../../components/ui/CardWrapper"; -import styles from "./TransactionsFabric.module.css"; -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -function TransactionsFabric() { - const navigate = useNavigate(); - const [transactions, setTransactions] = useState([]); - - const txnTableProps = { - onClick: { - action: (param: string) => { - navigate(`/fabric/txn-details/${param}`); - }, - prop: "id", - }, - schema: [ - { display: "created at", objProp: ["created_at"] }, - { display: "transaction id", objProp: ["transaction_id"] }, - { display: "channel name", objProp: ["channel_id"] }, - { display: "block id", objProp: ["block_id"] }, - { display: "status", objProp: ["status"] }, - ], - }; - - const fetchTransactions = async () => { - try { - const { data } = await supabase.from("fabric_transactions").select("*"); - if (data) { - setTransactions(data); - } else { - throw new Error("Failed to load transactions"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchTransactions(); - }, []); - - return ( -
- -
- ); -} - -export default TransactionsFabric; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/queries.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/queries.ts new file mode 100644 index 00000000000..48343c52ef2 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/queries.ts @@ -0,0 +1,201 @@ +/** + * File containing all react-query functions used by this app. + * @todo Move to separate directory if this file becomes too complex. + */ + +import { createClient } from "@supabase/supabase-js"; +import { queryOptions } from "@tanstack/react-query"; +import { + FabricBlock, + FabricCertificate, + FabricTransaction, + FabricTransactionAction, + FabricTransactionActionEndorsement, +} from "./fabric-supabase-types"; + +// TODO - Configure for an app +const supabaseQueryKey = "supabase:fabric"; +const supabaseUrl = "http://localhost:8000"; +const supabaseKey = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; +export const supabase = createClient(supabaseUrl, supabaseKey, { + schema: "fabric", +}); + +function createQueryKey( + tableName: string, + pagination: { page: number; pageSize: number }, +) { + return [tableName, { pagination }]; +} + +/** + * Get all recorded fabric blocks. + * Returns `queryOptions` to be used as argument to `useQuery` from `react-query`. + * Supports paging. + */ +export function fabricAllBlocksQuery(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 FabricBlock[]; + }, + }); +} + +/** + * Get all recorded fabric transactions. + * Returns `queryOptions` to be used as argument to `useQuery` from `react-query`. + * Supports paging. + */ +export function fabricAllTransactionsQuery(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 FabricTransaction[]; + }, + }); +} + +/** + * Get transaction object form the database using it's hash. + */ +export function fabricTransactionByHash(hash: string) { + const tableName = "transaction"; + + return queryOptions({ + queryKey: [supabaseQueryKey, tableName, hash], + queryFn: async () => { + const { data, error } = await supabase + .from(tableName) + .select() + .match({ hash }); + + if (error) { + throw new Error( + `Could not get transaction with hash ${hash}: ${error.message}`, + ); + } + + if (data.length !== 1) { + throw new Error( + `Invalid response when getting transaction with hash ${hash}: ${data}`, + ); + } + + return data.pop() as FabricTransaction; + }, + }); +} + +/** + * Get transaction actions form the database using parent transaction id. + */ +export function fabricTransactionActions(txId: string) { + const tableName = "transaction_action"; + + return queryOptions({ + queryKey: [supabaseQueryKey, tableName, txId], + queryFn: async () => { + const { data, error } = await supabase + .from(tableName) + .select() + .match({ transaction_id: txId }); + + if (error) { + throw new Error( + `Could not get actions of transaction with ID ${txId}: ${error.message}`, + ); + } + + return data as FabricTransactionAction[]; + }, + }); +} + +/** + * Get fabric certificate using it's ID. + */ +export function fabricCertificate(id: string) { + const tableName = "certificate"; + + return queryOptions({ + queryKey: [supabaseQueryKey, tableName, id], + queryFn: async () => { + const { data, error } = await supabase + .from(tableName) + .select() + .match({ id }); + + if (error) { + throw new Error( + `Could not get certificate with ID ${id}: ${error.message}`, + ); + } + + if (data.length !== 1) { + throw new Error( + `Invalid response when getting certificate with id ${id}: ${data}`, + ); + } + + return data.pop() as FabricCertificate; + }, + }); +} + +/** + * Get transaction action endorsements form the database using parent action id. + */ +export function fabricActionEndorsements(actionId: string) { + const tableName = "transaction_action_endorsement"; + + return queryOptions({ + queryKey: [supabaseQueryKey, tableName, actionId], + queryFn: async () => { + const { data, error } = await supabase + .from(tableName) + .select() + .match({ transaction_action_id: actionId }); + + if (error) { + throw new Error( + `Could not get endorsements of action with ID ${actionId}: ${error.message}`, + ); + } + + return data as FabricTransactionActionEndorsement[]; + }, + }); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/TitleWithIcon.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ui/TitleWithIcon.tsx new file mode 100644 index 00000000000..ce55791ac22 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/TitleWithIcon.tsx @@ -0,0 +1,24 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { SvgIconComponent } from "@mui/icons-material"; + +interface TitleWithIconProps { + icon: SvgIconComponent; + children: React.ReactNode; +} + +const TitleWithIcon: React.FC = ({ + children, + icon: Icon, +}) => { + return ( + + + + {children} + + + ); +}; + +export default TitleWithIcon; diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/ClickableTableRow.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/ClickableTableRow.tsx new file mode 100644 index 00000000000..c67f25dcedd --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/components/ui/UITableListing/ClickableTableRow.tsx @@ -0,0 +1,12 @@ +import TableRow from "@mui/material/TableRow"; +import { styled } from "@mui/material/styles"; + +export const ClickableTableRow = styled(TableRow)(({ theme }) => ({ + cursor: "pointer", + "&:hover": { + backgroundColor: theme.palette.grey[200], + "& .MuiTableCell-root": { + color: theme.palette.secondary.main, + }, + }, +})); 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 index 1ec138d12bd..6d206f87937 100644 --- 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 @@ -21,6 +21,7 @@ import { tableCellHeight, } from "./StyledTableCell"; import type { UITableListingPaginationActionProps } from "./UITableListingPaginationAction"; +import { ClickableTableRow } from "./ClickableTableRow"; /** * Table column configuration entry @@ -55,6 +56,7 @@ export interface UITableListingProps { columns: string[]; rowsPerPage: number; tableSize?: "small" | "medium"; + onClick?: (row: Record) => void; } function getKeyField(columnConfig: ColumnConfigType) { @@ -83,6 +85,22 @@ function formatCellValue(config: ColumnConfigEntry, value: any) { return value; } +function createTableRow( + row: Record, + columns: string[], + columnConfig: ColumnConfigType, +) { + return columns.map((colName) => { + const config = columnConfig[colName]; + const value = row[config.field]; + return ( + + {formatCellValue(config, value)} + + ); + }); +} + /** * UITableListing - Show table with paged data fetched from react-query. * @@ -106,6 +124,7 @@ export default function UITableListing< columns, rowsPerPage, tableSize, + onClick, }: UITableListingProps) { const [page, setPage] = React.useState(0); const { isError, isPending, data, error, refetch } = useQuery({ @@ -154,19 +173,24 @@ export default function UITableListing< - {displayData.map((row) => ( - - {columns.map((colName) => { - const config = columnConfig[colName]; - const value = row[config.field]; - return ( - - {formatCellValue(config, value)} - - ); - })} - - ))} + {displayData.map((row) => { + if (onClick) { + return ( + onClick(row)} + > + {createTableRow(row, columns, columnConfig)} + + ); + } else { + return ( + + {createTableRow(row, columns, columnConfig)} + + ); + } + })} {emptyRows > 0 && ( diff --git a/yarn.lock b/yarn.lock index c8fb00bf494..493b6b766d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9059,6 +9059,7 @@ __metadata: "@types/react-dom": "npm:18.2.17" "@vitejs/plugin-react": "npm:4.2.1" apexcharts: "npm:3.45.2" + buffer: "npm:6.0.3" ethers: "npm:6.12.1" react: "npm:18.2.0" react-apexcharts: "npm:1.4.1"