Skip to content

feat(ledger-browser): refactor home page #3340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 7, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/cacti-ledger-browser/README.md
Original file line number Diff line number Diff line change
@@ -46,7 +46,9 @@ npm install
- Run one or more persistence plugins:
- [Ethereum](../cacti-plugin-persistence-ethereum)
- [Fabric] (../cacti-plugin-persistence-fabric)
- Edit [Supabase configuration file](./src/supabase-client.tsx), set correct supabase API URL and service_role key.
- Edit Supabase configuration files, set correct supabase API URL and service_role key.
- ./src/main/typescript/common/supabase-client.tsx
- ./src/main/typescript/common/queries.ts
- Execute `yarn run start` or `npm start` in this package directory.
- The running application address: http://localhost:3001/ (can be changed in [Vite configuration](./vite.config.ts))

10 changes: 0 additions & 10 deletions packages/cacti-ledger-browser/package.json
Original file line number Diff line number Diff line change
@@ -38,16 +38,6 @@
"name": "Tomasz Awramski",
"email": "tomasz.awramski@fujitsu.com",
"url": "https://www.fujitsu.com/global/"
},
{
"name": "Eryk Baranowski",
"email": "eryk.baranowski@fujitsu.com",
"url": "https://www.fujitsu.com/global/"
},
{
"name": "Barnaba Pawelczak",
"email": "barnaba.pawelczak@fujitsu.com",
"url": "https://www.fujitsu.com/global/"
}
],
"scripts": {
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { themeOptions } from "./theme";
import ContentLayout from "./components/Layout/ContentLayout";
import HeaderBar from "./components/Layout/HeaderBar";
import WelcomePage from "./components/WelcomePage";
import HomePage from "./pages/home/HomePage";
import { AppConfig, AppListEntry } from "./common/types/app";
import { patchAppRoutePath } from "./common/utils";
import { NotificationProvider } from "./common/context/NotificationContext";
@@ -22,8 +22,8 @@ type AppConfigProps = {
function getAppList(appConfig: AppConfig[]) {
const appList: AppListEntry[] = appConfig.map((app) => {
return {
path: app.path,
name: app.name,
path: app.options.path,
name: app.appName,
};
});

@@ -43,12 +43,12 @@ function getHeaderBarRoutes(appConfig: AppConfig[]) {

const headerRoutesConfig = appConfig.map((app) => {
return {
key: app.path,
path: `${app.path}/*`,
key: app.options.path,
path: `${app.options.path}/*`,
element: (
<HeaderBar
appList={appList}
path={app.path}
path={app.options.path}
menuEntries={app.menuEntries}
/>
),
@@ -68,12 +68,12 @@ function getHeaderBarRoutes(appConfig: AppConfig[]) {
function getContentRoutes(appConfig: AppConfig[]) {
const appRoutes: RouteObject[] = appConfig.map((app) => {
return {
key: app.path,
path: app.path,
key: app.options.path,
path: app.options.path,
children: app.routes.map((route) => {
return {
key: route.path,
path: patchAppRoutePath(app.path, route.path),
path: patchAppRoutePath(app.options.path, route.path),
element: route.element,
children: route.children,
};
@@ -84,7 +84,7 @@ function getContentRoutes(appConfig: AppConfig[]) {
// Include landing / welcome page
appRoutes.push({
index: true,
element: <WelcomePage />,
element: <HomePage />,
});

return useRoutes([

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
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 Accounts from "./pages/Accounts/Accounts";
import { AppConfig } from "../../common/types/app";
import { usePersistenceAppStatus } from "../../common/hook/use-persistence-app-status";
import PersistencePluginStatus from "../../components/PersistencePluginStatus/PersistencePluginStatus";

const ethConfig: AppConfig = {
name: "Ethereum",
path: "/eth",
appName: "Ethereum Browser",
options: {
instanceName: "Ethereum",
description:
"Applicaion for browsing Ethereum ledger blocks, transactions and tokens. Requires Ethereum persistence plugin to work correctly.",
path: "/eth",
},
menuEntries: [
{
title: "Dashboard",
@@ -34,6 +41,10 @@ const ethConfig: AppConfig = {
element: <Accounts />,
},
],
useAppStatus: () => usePersistenceAppStatus("PluginPersistenceEthereum"),
StatusComponent: (
<PersistencePluginStatus pluginName="PluginPersistenceEthereum" />
),
};

export default ethConfig;
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import TextField from "@mui/material/TextField";
import { styled } from "@mui/material/styles";

import { FabricCertificate } from "../../fabric-supabase-types";
import StackedRowItems from "../ui/StackedRowItems";
import StackedRowItems from "../../../../components/ui/StackedRowItems";

const ListHeaderTypography = styled(Typography)(({ theme }) => ({
color: theme.palette.secondary.main,
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
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";
import { AppConfig } from "../../common/types/app";
import { usePersistenceAppStatus } from "../../common/hook/use-persistence-app-status";
import PersistencePluginStatus from "../../components/PersistencePluginStatus/PersistencePluginStatus";

const fabricConfig: AppConfig = {
name: "Fabric",
path: "/fabric",
appName: "Hyperledger Fabric Browser",
options: {
instanceName: "Fabric",
description:
"Applicaion for browsing Hyperledger Fabric ledger blocks and transactions. Requires Fabric persistence plugin to work correctly.",
path: "/fabric",
},
menuEntries: [
{
title: "Dashboard",
@@ -41,6 +48,10 @@ const fabricConfig: AppConfig = {
],
},
],
useAppStatus: () => usePersistenceAppStatus("PluginPersistenceFabric"),
StatusComponent: (
<PersistencePluginStatus pluginName="PluginPersistenceFabric" />
),
};

export default fabricConfig;
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import Skeleton from "@mui/material/Skeleton";

import { FabricTransaction } from "../../fabric-supabase-types";
import ShortenedTypography from "../../../../components/ui/ShortenedTypography";
import StackedRowItems from "../../components/ui/StackedRowItems";
import StackedRowItems from "../../../../components/ui/StackedRowItems";

const ListHeaderTypography = styled(Typography)(({ theme }) => ({
color: theme.palette.secondary.main,
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import cactiGuiConfig from "../apps/cacti/index";
import ethereumGuiConfig from "../apps/eth";
import fabricAppConfig from "../apps/fabric";
import { AppConfig } from "./types/app";

export const appConfig: AppConfig[] = [
cactiGuiConfig,
ethereumGuiConfig,
fabricAppConfig,
];
export const appConfig: AppConfig[] = [ethereumGuiConfig, fabricAppConfig];
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";
import { useQuery } from "@tanstack/react-query";
import { GetStatusResponse } from "../types/app";
import { useNotification } from "../context/NotificationContext";
import { persistencePluginStatus } from "../queries";

/**
* Return status of given persistence plugin from the database.
*
* @param pluginName name of the plugin (as set by the persistence plugin itself)
*/
export function usePersistenceAppStatus(pluginName: string): GetStatusResponse {
const { isError, isPending, data, error } = useQuery(
persistencePluginStatus(pluginName),
);
const { showNotification } = useNotification();

React.useEffect(() => {
isError &&
showNotification(`Could get ${pluginName} status: ${error}`, "error");
}, [isError]);

return {
isPending,
isInitialized: data?.is_schema_initialized ?? false,
status: {
severity: "info",
message: "Unknown",
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createClient } from "@supabase/supabase-js";
import { queryOptions } from "@tanstack/react-query";
import { PluginStatus } from "./supabase-types";

const supabaseQueryKey = "supabase";
const supabaseUrl = "__SUPABASE_URL__";
const supabaseKey = "__SUPABASE_KEY__";

export const supabase = createClient(supabaseUrl, supabaseKey);

/**
* Get persistence plugin status from the database using it's name.
*/
export function persistencePluginStatus(name: string) {
const tableName = "plugin_status";

return queryOptions({
queryKey: [supabaseQueryKey, tableName, name],
queryFn: async () => {
const { data, error } = await supabase
.from(tableName)
.select()
.match({ name });

if (error) {
throw new Error(
`Could not get persistence plugin status with name ${name}: ${error.message}`,
);
}

if (data.length !== 1) {
throw new Error(
`Invalid response when persistence plugin status with name ${name}: ${data}`,
);
}

return data.pop() as PluginStatus;
},
});
}
Original file line number Diff line number Diff line change
@@ -2,9 +2,9 @@ import { createClient } from "@supabase/supabase-js";
import { queryOptions } from "@tanstack/react-query";

export const supabaseQueryKey = "supabase";
const supabaseUrl = "http://localhost:8000";
const supabaseKey =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE";
const supabaseUrl = "__SUPABASE_URL__";
const supabaseKey = "__SUPABASE_KEY__";

export const supabase = createClient(supabaseUrl, supabaseKey);

/**
Original file line number Diff line number Diff line change
@@ -108,3 +108,11 @@ export interface TokenERC20 {
total_supply: number;
token_address: string;
}

export interface PluginStatus {
name: string;
last_instance_id: string;
is_schema_initialized: boolean;
created_at: string;
last_connected_at: string;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from "react";
import { RouteObject } from "react-router-dom";

export interface AppListEntry {
@@ -10,9 +11,28 @@ export interface AppConfigMenuEntry {
url: string;
}

export interface AppConfig {
name: string;
export interface AppStatus {
severity: "success" | "info" | "warning" | "error";
message: string;
}

export interface GetStatusResponse {
isPending: boolean;
isInitialized: boolean;
status: AppStatus;
}

export interface AppConfigOptions {
instanceName: string;
description: string | undefined;
path: string;
}

export interface AppConfig {
appName: string;
options: AppConfigOptions;
menuEntries: AppConfigMenuEntry[];
routes: RouteObject[];
useAppStatus: () => GetStatusResponse;
StatusComponent: React.ReactElement;
}
Original file line number Diff line number Diff line change
@@ -4,14 +4,9 @@ import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import AppsIcon from "@mui/icons-material/Apps";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import Drawer from "@mui/material/Drawer";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText";
import { AppConfigMenuEntry, AppListEntry } from "../../common/types/app";
import { patchAppRoutePath } from "../../common/utils";

@@ -21,31 +16,7 @@ type HeaderBarProps = {
menuEntries?: AppConfigMenuEntry[];
};

const HeaderBar: React.FC<HeaderBarProps> = ({
appList,
path,
menuEntries,
}) => {
const [isAppSelectOpen, setIsAppSelectOpen] = React.useState(false);

const AppSelectDrawer = (
<Box
sx={{ width: 250 }}
role="presentation"
onClick={() => setIsAppSelectOpen(false)}
>
<List>
{appList.map((app) => (
<ListItem key={app.name} disablePadding>
<ListItemButton component={RouterLink} to={app.path}>
<ListItemText primary={app.name} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
);

const HeaderBar: React.FC<HeaderBarProps> = ({ path, menuEntries }) => {
return (
<AppBar position="static" sx={{ paddingX: 2 }}>
<Toolbar disableGutters>
@@ -56,9 +27,10 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
color="inherit"
aria-label="select-application-button"
sx={{ mr: 2 }}
onClick={() => setIsAppSelectOpen(true)}
component={RouterLink}
to={"/"}
>
<MenuIcon />
<AppsIcon />
</IconButton>
</Tooltip>

@@ -77,10 +49,6 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
</Box>
)}
</Toolbar>

<Drawer open={isAppSelectOpen} onClose={() => setIsAppSelectOpen(false)}>
{AppSelectDrawer}
</Drawer>
</AppBar>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from "react";
import { useQuery } from "@tanstack/react-query";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import CircularProgress from "@mui/material/CircularProgress";

import StackedRowItems from "../ui/StackedRowItems";
import { persistencePluginStatus } from "../../common/queries";
import { useNotification } from "../../common/context/NotificationContext";

type DateTimeStringProps = {
dateString: string | undefined;
};

function DateTimeString({ dateString }: DateTimeStringProps) {
const date = dateString ? new Date(dateString) : new Date();

return <Typography>{date.toLocaleString()}</Typography>;
}

type PersistencePluginStatusProps = {
pluginName: string;
};

/**
* Box that fetches and displays persistence plugin status from the database.
*/
export default function PersistencePluginStatus({
pluginName,
}: PersistencePluginStatusProps) {
const { isError, isPending, data, error } = useQuery(
persistencePluginStatus(pluginName),
);
const { showNotification } = useNotification();

React.useEffect(() => {
isError &&
showNotification(`Could get ${pluginName} status: ${error}`, "error");
}, [isError]);

return (
<Box>
{isPending && (
<CircularProgress
style={{
position: "absolute",
top: "50%",
left: "50%",
zIndex: 9999,
}}
/>
)}
<Typography variant="h5">Persistence Plugin Status</Typography>
<StackedRowItems>
<Typography>Plugin Name:</Typography>
<Typography>{data?.name}</Typography>
</StackedRowItems>
<StackedRowItems>
<Typography>Instance ID:</Typography>
<Typography>{data?.last_instance_id}</Typography>
</StackedRowItems>
<StackedRowItems>
<Typography>Is Schema Initialized:</Typography>
<Typography>
{data?.is_schema_initialized ? "True" : "False"}
</Typography>
</StackedRowItems>
<StackedRowItems>
<Typography>Created At:</Typography>
<DateTimeString dateString={data?.created_at} />
</StackedRowItems>
<StackedRowItems>
<Typography>Last Connected At:</Typography>
<DateTimeString dateString={data?.last_connected_at} />
</StackedRowItems>
</Box>
);
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

3 changes: 3 additions & 0 deletions packages/cacti-ledger-browser/src/main/typescript/main.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Needed to fix vite caching error of MUI - see https://github.com/vitejs/vite/issues/12423
import "@mui/material/styles/styled";

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import { appConfig } from "./common/config";
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import Card from "@mui/material/Card";
import CardActionArea from "@mui/material/CardActionArea";
import CardActions from "@mui/material/CardActions";
import CardContent from "@mui/material/CardContent";
import CircularProgress from "@mui/material/CircularProgress";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";

import { AppConfig, AppStatus } from "../../common/types/app";

type StatusTextProps = {
status: AppStatus;
};

/**
* Application status text with color according to it's severity.
*/
function StatusText({ status }: StatusTextProps) {
const theme = useTheme();

return (
<span style={{ color: theme.palette[status.severity].main }}>
{status.message}
</span>
);
}

type InitializedTextProps = {
isInitialized: boolean;
};

/**
* Application initialization status text - `error` color if not initialized, `success` otherwise.
*/
function InitializedText({ isInitialized }: InitializedTextProps) {
let text = "No";
let textColor: "error" | "success" = "error";

if (isInitialized) {
text = "Yes";
textColor = "success";
}

return <StatusText status={{ severity: textColor, message: text }} />;
}

type StatusDialogButtonProps = {
statusComponent: React.ReactElement;
};

function StatusDialogButton({ statusComponent }: StatusDialogButtonProps) {
const [openDialog, setOpenDialog] = React.useState(false);

return (
<>
<Button onClick={() => setOpenDialog(true)}>Status</Button>
<Dialog
fullWidth
maxWidth="sm"
onClose={() => setOpenDialog(false)}
open={openDialog}
>
<DialogTitle color="primary">App Status</DialogTitle>
<DialogContent>{statusComponent}</DialogContent>
</Dialog>
</>
);
}

type AppCardProps = {
appConfig: AppConfig;
};

/**
* Application card component. Shows basic information and allows navigation to
* specific app on click. Has action for showing app status and configuration
* pop-ups.
*/
export default function AppCard({ appConfig }: AppCardProps) {
const navigate = useNavigate();
const theme = useTheme();
const status = appConfig.useAppStatus();

return (
<Card
variant="outlined"
sx={{
display: "flex",
flexDirection: "column",
width: 400,
}}
>
<CardActionArea
onClick={() => {
navigate(appConfig.options.path);
}}
>
<CardContent
sx={{
flex: 1,
paddingBottom: 1,
}}
>
<Typography variant="h5" component="div" color="secondary.main">
{appConfig.options.instanceName}
</Typography>
<Typography sx={{ mb: 1.5 }} color="text.secondary">
{appConfig.appName}
</Typography>
{appConfig.options.description && (
<Typography sx={{ mb: 1.5 }}>
{appConfig.options.description}
</Typography>
)}
<Typography>
Initialized:{" "}
{status.isPending ? (
<CircularProgress size={17} />
) : (
<InitializedText isInitialized={status.isInitialized} />
)}
</Typography>
<Typography>
Status:{" "}
{status.isPending ? (
<CircularProgress size={17} />
) : (
<StatusText status={status.status} />
)}
</Typography>
</CardContent>
</CardActionArea>
<CardActions
sx={{
marginTop: 0,
justifyContent: "right",
borderTop: 1,
borderColor: theme.palette.primary.main,
}}
>
<StatusDialogButton statusComponent={appConfig.StatusComponent} />
</CardActions>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

import { appConfig } from "../../common/config";
import AppCard from "./AppCard";

export default function HomePage() {
return (
<Box>
<Typography variant="h5" color="secondary">
Applications
</Typography>
<Box
display="flex"
flexWrap="wrap"
justifyContent="space-around"
gap={5}
padding={5}
>
{appConfig.map((a) => {
return (
<AppCard
key={`${a.appName}_${a.options.instanceName}`}
appConfig={a}
/>
);
})}
</Box>
</Box>
);
}
3 changes: 3 additions & 0 deletions packages/cacti-ledger-browser/src/main/typescript/theme.ts
Original file line number Diff line number Diff line change
@@ -12,5 +12,8 @@ export const themeOptions: ThemeOptions = {
warning: {
main: "#D68C45",
},
info: {
main: "#5D4037",
},
},
};