Skip to content

Commit

Permalink
feat(ledger-browser): rewrite fabric application
Browse files Browse the repository at this point in the history
- 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 hyperledger-cacti#3308
Depends on hyperledger-cacti#3279

Signed-off-by: Michal Bajer <michal.bajer@fujitsu.com>
  • Loading branch information
outSH committed Jul 24, 2024
1 parent 1ce7c60 commit ecf074c
Show file tree
Hide file tree
Showing 38 changed files with 1,379 additions and 661 deletions.
1 change: 1 addition & 0 deletions packages/cacti-ledger-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TitleWithIconProps> = ({
children,
icon: Icon,
}) => {
return (
<Box display="flex" alignItems="center" marginBottom={2}>
<Icon sx={{ fontSize: 35 }} color="primary" />
<Typography variant="h6" component="h3" marginLeft={1}>
{children}
</Typography>
</Box>
);
};

function Dashboard() {
return (
<Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UITableListingPaginationActionProps>;
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<BlockListProps> = ({
footerComponent,
columns,
rowsPerPage,
tableSize,
}) => {
return (
<UITableListing
queryFunction={fabricAllBlocksQuery}
label="block"
columnConfig={blockColumnsConfig}
footerComponent={footerComponent}
columns={columns}
rowsPerPage={rowsPerPage}
tableSize={tableSize}
/>
);
};

export default BlockList;
Original file line number Diff line number Diff line change
@@ -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",
},
};
Original file line number Diff line number Diff line change
@@ -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 (
<ul style={{ margin: 0 }}>
<li>
<StackedRowItems>
<ListHeaderTypography>Common Name:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(certificate[`${fieldPrefix}_common_name`])}
</Typography>
</StackedRowItems>
</li>
<li>
<StackedRowItems>
<ListHeaderTypography>Organization:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(certificate[`${fieldPrefix}_org`])}
</Typography>
</StackedRowItems>
</li>
<li>
<StackedRowItems>
<ListHeaderTypography>Organization Unit:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(certificate[`${fieldPrefix}_org_unit`])}
</Typography>
</StackedRowItems>
</li>
<li>
<StackedRowItems>
<ListHeaderTypography>Country:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(certificate[`${fieldPrefix}_country`])}
</Typography>
</StackedRowItems>
</li>
<li>
<StackedRowItems>
<ListHeaderTypography>Locality:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(certificate[`${fieldPrefix}_locality`])}
</Typography>
</StackedRowItems>
</li>
<li>
<StackedRowItems>
<ListHeaderTypography>State:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(certificate[`${fieldPrefix}_state`])}
</Typography>
</StackedRowItems>
</li>
</ul>
);
}

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 (
<Box>
<StackedRowItems>
<ListHeaderTypography>Serial Number:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(certificate?.serial_number)}
</Typography>
</StackedRowItems>
<StackedRowItems>
<ListHeaderTypography>Valid From:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(formatDateString(certificate?.valid_from))}
</Typography>
</StackedRowItems>
<StackedRowItems>
<ListHeaderTypography>Valid To:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(formatDateString(certificate?.valid_to))}
</Typography>
</StackedRowItems>
<StackedRowItems>
<ListHeaderTypography>Alt Name:</ListHeaderTypography>
<Typography>
{formatCertificateAttr(certificate?.subject_alt_name)}
</Typography>
</StackedRowItems>

{certificate ? (
<>
<ListHeaderTypography marginTop={2}>Subject:</ListHeaderTypography>
{formatCertificateSubject(certificate, "subject")}
</>
) : (
<StackedRowItems>
<ListHeaderTypography>Subject:</ListHeaderTypography>
<Typography>-</Typography>
</StackedRowItems>
)}

{certificate ? (
<>
<ListHeaderTypography marginTop={2}>Issuer:</ListHeaderTypography>
{formatCertificateSubject(certificate, "issuer")}
</>
) : (
<StackedRowItems>
<ListHeaderTypography>Issuer:</ListHeaderTypography>
<Typography>-</Typography>
</StackedRowItems>
)}

<ListHeaderTypography marginTop={2}>
Certificate (PEM):
</ListHeaderTypography>
<TextFieldDisabledBlackFont
disabled
fullWidth
rows={5}
multiline
size="small"
value={certificate?.pem ?? ""}
/>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Button
disabled={!data}
style={{ textTransform: "none" }}
variant="outlined"
onClick={() => setOpenDialog(true)}
>
{creatorName}@{creatorOrg}
</Button>
<Dialog onClose={() => setOpenDialog(false)} open={openDialog}>
<DialogTitle color="primary">Certificate Details</DialogTitle>
<DialogContent>
<CertificateDetailsBox certificate={data} />
</DialogContent>
</Dialog>
</>
);
}

This file was deleted.

Loading

0 comments on commit ecf074c

Please sign in to comment.