diff --git a/docs/docs/cactus/ledger-browser/developer-guide/architecture.md b/docs/docs/cactus/ledger-browser/developer-guide/architecture.md new file mode 100644 index 0000000000..740c1a982d --- /dev/null +++ b/docs/docs/cactus/ledger-browser/developer-guide/architecture.md @@ -0,0 +1,143 @@ +# Architecture + +## Components + +### AppDefinition + +Each application must define an `AppDefinition`. This includes essential information like the app's name, category, and default settings shown to the user during app creation (such as description, path, and custom options). The most critical part of the `AppDefinition` is the `createAppInstance` factory method. This method accepts a `GuiAppConfig` (which contains dynamic app settings configured by the user and stored in a database) and returns an `AppInstance` object. + +#### Interface + +```typescript +interface AppDefinition { + /** + * Application name as shown to the user + */ + appName: string; + + /** + * Application category, the user can filter using it. + * If there's no matching category for your app consider adding a new one! + */ + category: string; + + /** + * Full URL to a setup guide, it will be displayed to the user on app configuration page. + */ + appSetupGuideURL?: string; + + /** + * Full URL to app documentation page + */ + appDocumentationURL?: string; + + /** + * Default value for instance name that user can set to uniquely identify this ap instance. + */ + defaultInstanceName: string; + + /** + * Default value for app description. + */ + defaultDescription: string; + + /** + * Default path under which app routes will be mounted (must be path with `/`, like `/eth`) + */ + defaultPath: string; + + /** + * Default custom, app-specific options in JSON format. This will change between applications. + */ + defaultOptions: unknown; + + /** + * Factory method for creating application instance object from configuration stored in a database. + */ + createAppInstance: CreateAppInstanceFactoryType; +} +``` + +### AppInstance + +An `AppInstance` represents the runtime configuration of an app. It holds all the details required by `Ledger Browser` to render the app correctly. Key components include `menuEntries` (links displayed in the top header bar) and `routes` that adhere to [react-router-dom](https://reactrouter.com/en/main) specifications. + +```typescript +interface AppInstance { + /** + * Unique database ID of this app instance. + */ + id: string; + + /** + * Name of the application (can be same as appName in app definition) + */ + appName: string; + + /** + * Instance name (set by the user) + */ + instanceName: string; + + /** + * Instance description (set by the user) + */ + description: string | undefined; + + /** + * Path under which app routes will be mounted (must be path with `/`, like `/eth`) + */ + path: string; + + /** + * Custom, app-specific options in JSON format. This will change between applications. + */ + options: T; + + /** + * List on titles and URL of menu entries to be added to the top bar (used to navigate within an app) + */ + menuEntries: AppInstanceMenuEntry[]; + + /** + * `react-router-dom` compatible list of this application routes. + */ + routes: RouteObject[]; + + /** + * Method for retriving application status details. + */ + useAppStatus: () => GetStatusResponse; + + /** + * Status component showed when user opens a app status pop up window. + */ + StatusComponent: React.ReactElement; + + /** + * Full URL to a setup guide, it will be displayed to the user on app configuration page. + */ + appSetupGuideURL?: string; + + /** + * Full URL to app documentation page + */ + appDocumentationURL?: string; +} +``` + +### AppConfig + +To make an application available to the `Ledger Browser`, it must be added to the main `AppConfig` mapping (located in `common/config.tsx`). When creating a new app, ensure it is included here, and assign it a unique ID within the map. + +### ConfiguredApps (GuiAppConfig) + +`ConfiguredApps` refers to the dynamic settings for apps as configured by the user and stored in a database. This data is maintained in the `public.gui_app_config` table within the GUI's PostgreSQL instance. + +### AppInstance List + +During startup, the application reads dynamic app configurations from the database (`GuiAppConfig`), retrieves the corresponding `AppDefinition` from the main `AppConfig`, and uses the `createAppInstance` method to generate the final `AppInstance`. This instance is then added to a list, with its routes mounted under the specified path. + +## Diagram + +![Ledger Browser Architecture Diagram](images/architecture_cacti_ledger_browser_architecture.png) \ No newline at end of file diff --git a/docs/docs/cactus/ledger-browser/developer-guide/images/architecture_cacti_ledger_browser_architecture.png b/docs/docs/cactus/ledger-browser/developer-guide/images/architecture_cacti_ledger_browser_architecture.png new file mode 100644 index 0000000000..46b157212e Binary files /dev/null and b/docs/docs/cactus/ledger-browser/developer-guide/images/architecture_cacti_ledger_browser_architecture.png differ diff --git a/docs/docs/cactus/ledger-browser/developer-guide/overview.md b/docs/docs/cactus/ledger-browser/developer-guide/overview.md new file mode 100644 index 0000000000..9c723ae4da --- /dev/null +++ b/docs/docs/cactus/ledger-browser/developer-guide/overview.md @@ -0,0 +1,18 @@ +# Developer Guide Overview + +Ledger Browser is designed with extensibility and customization in mind. You can enhance the GUI functionality by developing custom apps that users can configure and use. + +## Limitations + +Currently, applications must be included in the ledger-browser source code and built together with the main application. As the number of apps grows, we may consider transitioning to a more modular (though also more complex) design, allowing apps to be developed in separate packages. + +## Guidelines + +- Use React functional components. +- Use TypeScript and adhere to the [global Cacti linter requirements](https://github.com/hyperledger/cacti/blob/main/.eslintrc.js). +- Whenever possible, utilize [MaterialUI](https://mui.com/) and [common UI components](https://github.com/hyperledger/cacti/tree/main/packages/cacti-ledger-browser/src/main/typescript/components/ui). +- Use [PageTitle](https://github.com/hyperledger/cacti/tree/main/packages/cacti-ledger-browser/src/main/typescript/components/ui/PageTitle.tsx) as main page title. +- Use [PageTitleWithGoBack](https://github.com/hyperledger/cacti/tree/main/packages/cacti-ledger-browser/src/main/typescript/components/ui/PageTitleWithGoBack.tsx) as subpage title (it has arrow attached for going back). +- Use [NotificationContext](https://github.com/hyperledger/cacti/blob/main/packages/cacti-ledger-browser/src/main/typescript/common/context/NotificationContext.tsx) for displaying Pop-Up windows with notifications (info, success, error, etc..). +- App routes are defined using [react-router-dom](https://reactrouter.com/en/main). +- Use [react-query](https://tanstack.com/query/v3) for fetching data, `QueryClientProvider` is already available in the application. diff --git a/docs/docs/cactus/ledger-browser/developer-guide/tutorial.md b/docs/docs/cactus/ledger-browser/developer-guide/tutorial.md new file mode 100644 index 0000000000..b419dc3bdf --- /dev/null +++ b/docs/docs/cactus/ledger-browser/developer-guide/tutorial.md @@ -0,0 +1,329 @@ +# Tutorial - Adding new application + +This tutorial will show you step by step how to create a new app inside ledger browser. The app we're going to create will be very simple and static because it's purpose is just to ilustrate the steps and is meant as a starting point for your application. + +## Setup Ledger Browser + +Follow our setup guide to run Supabase instance and start the Ledger Browser. + +## Create basic app structure + +All applications should be located in the `apps` directory within the ledger-browser source code. Create a new directory for your app and create its main entry point as `index.tsx`. + +```shell +# We assume you're at the root of cacti repository +cd packages/cacti-ledger-browser/src/main/typescript/apps/ +mkdir my-tutorial-app +cd my-tutorial-app +code index.tsx # Use any editor you want, this code assumes Visual Studio Code is used +``` + +Each app should define an `index.tsx` file that includes its `AppDefinition`. + +```typescript +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { AppDefinition } from "../../common/types/app"; +import { GuiAppConfig } from "../../common/supabase-types"; +import { AppCategory } from "../../common/app-category"; + +const myTutorialAppDefinition: AppDefinition = { + appName: "My Tutorial App", + category: AppCategory.SampleApp, + defaultInstanceName: "My App", + defaultDescription: "This is a tutorial application.", + defaultPath: "/my-tutorial", + defaultOptions: { + // Our app will use this variable to display a greeting later on + name: "Cacti", + }, + + createAppInstance(app: GuiAppConfig) { + if (!app.options || !app.options.name) { + throw new Error(`Missing 'name' in received GuiAppConfig options!`); + } + + return { + id: app.id, + appName: myTutorialAppDefinition.appName, + instanceName: app.instance_name, + description: app.description, + path: app.path, + options: app.options, + menuEntries: [], + routes: [], + useAppStatus: () => { + return { + isPending: false, + isInitialized: true, + status: { + severity: "success", + message: "Mocked response!", + }, + }; + }, + StatusComponent: ( + + Everything is OK (we hope) + + ), + appSetupGuideURL: myTutorialAppDefinition.appSetupGuideURL, + appDocumentationURL: myTutorialAppDefinition.appDocumentationURL, + }; + }, +}; + +export default myTutorialAppDefinition; +``` + +Add the newly created application to the main `AppConfig` mapping. + +```shell +code ../../common/config.tsx +``` + +```typescript +import myTutorialAppDefinition from "../apps/my-tutorial-app"; +import { AppDefinition } from "./types/app"; + +const config = new Map([ + // ... + + // Add new application + ["myTutorialApplication", myTutorialAppDefinition], +]); + +export default config; +``` + +(Re)start the `Ledger Browser` application and add the newly created app by clicking the `Add Application` card on the main page. Select the `Sample App` category and choose `My Tutorial App`. On the `App Specific Setup` page, enter your name and click `Save`. The application configuration should be stored in the database, and the new application should appear on the main page. Clicking the `Status` button should display the hardcoded response defined in `StatusComponent`, and opening the app should navigate to [http://localhost:3001/my-tutorial](http://localhost:3001/my-tutorial), where nothing will be shown since no pages have been created yet. + +## Add the Home page + +Now it's time to add an actual page to our application. Our Home page will display a simple "Hello World" greeting using the name provided in the app configuration. To begin, create a new page component. Note that we're using the `PageTitle` component from the common UI components to maintain visual consistency with the rest of the application. + +```shell +mkdir pages +code pages/Home.tsx +``` + +```typescript +import Box from "@mui/material/Box"; +import PageTitle from "../../../components/ui/PageTitle"; + +export default function Home() { + return ( + + Hello World! + + ); +} +``` + +Next, we will add our Home page to the `AppDefinition` by including it in the routes and menu entries. + +```typescript +// ... +import Home from "./pages/Home"; + + +const myTutorialAppDefinition: AppDefinition = { + // ... + + createAppInstance(app: GuiAppConfig) { + // ... + return { + // ... + menuEntries: [ + { + title: "Home", + url: "/", + }, + ], + routes: [ + { + element: , + }, + ], + // ... + }; + }, +}; +``` + +Navigating to [http://localhost:3001/my-tutorial](http://localhost:3001/my-tutorial) should now display our "Hello World" message! To show a more personalized greeting, we need to access the custom app options stored in the app's React Router outer context. To simplify the retrieval of these options, we'll create a custom hook. + +```shell +code hooks.tsx +``` + +```typescript +import { useOutletContext } from "react-router-dom"; + +type TutorialOptionsType = { + name: string; +}; + +export function useAppOptions() { + return useOutletContext(); +} +``` + +Now we can use our custom hook to retrieve the name and display it to the user. + +```typescript +import Box from "@mui/material/Box"; +import PageTitle from "../../../components/ui/PageTitle"; +import { useAppOptions } from "../hooks"; + +export default function Home() { + const appOptions = useAppOptions(); + + return ( + + Hello {appOptions.name}! + + ); +} +``` + +The home page should now display a personalized greeting. + +## Data fetching and displaying notifications + +To illustrate how to fetch data from an external server, we'll create another page. We will use [restful-api.dev](https://restful-api.dev/) as a dummy backend for our requests. Begin by creating a new page called `DataFetch`. + +```shell +code pages/DataFetch.tsx +``` + +```typescript +import Box from "@mui/material/Box"; +import PageTitle from "../../../components/ui/PageTitle"; + +export default function DataFetch() { + return ( + + Data Fetch Sample + + ); +} +``` + +Next, add the new `DataFetch` page to the `AppDefinition`. + +```typescript +// ... +import DataFetch from "./pages/DataFetch"; + + +const myTutorialAppDefinition: AppDefinition = { + // ... + + createAppInstance(app: GuiAppConfig) { + // ... + return { + // ... + menuEntries: [ + { + title: "Home", + url: "/", + }, + { + title: "Data Fetch", + url: "/data-fetch", + }, + ], + routes: [ + { + element: , + }, + { + path: "data-fetch", + element: , + }, + ], + // ... + }; + }, +}; +``` + +Ensure that the header bar now includes a link to "Data Fetch" and that it correctly navigates to the newly created page at [http://localhost:3001/my-tutorial/data-fetch](http://localhost:3001/my-tutorial/data-fetch). Next, we'll use [react-query](https://tanstack.com/query/v3) to fetch data from the test REST server and display it on the page. + +```typescript +import axios from "axios"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import PageTitle from "../../../components/ui/PageTitle"; +import { useQuery } from "@tanstack/react-query"; + +/** + * Simple method for fetching test data from restful-api.dev + */ +async function fetchSampleData() { + const response = await axios.get("https://api.restful-api.dev/objects/7"); + return response.data; +} + +export default function DataFetch() { + const { data } = useQuery({ + queryKey: ["sampleFetch"], + queryFn: fetchSampleData, + }); + + return ( + + Data Fetch Sample + + + Fetched object: {data?.name ?? ""} + + + ); +} +``` + +You should see some object name displayed after the `Fetched object` text. Next, we'll display a success notification when our query finishes using the `useEffect` React hook. + +```typescript +import React from "react"; +import { useNotification } from "../../../common/context/NotificationContext"; +// ... + +export default function DataFetch() { + const { showNotification } = useNotification(); + const { data, isPending } = useQuery({ + queryKey: ["sampleFetch"], + queryFn: fetchSampleData, + }); + + React.useEffect(() => { + !isPending && + data && + showNotification(`Fetched data: ${data.name}`, "success"); + }, [data, isPending]); + + // ... +} +``` + +Try refreshing the page, and you should see a green notification in the bottom left corner with the fetched data name. Lastly, we'll handle query failures by displaying an error notification in such cases. + +```typescript +const { data, isPending, isError, error } = useQuery({ + queryKey: ["sampleFetch"], + queryFn: fetchSampleData, +}); + +React.useEffect(() => { + isError && showNotification(`Could not fetch sample data: ${error}`, "error"); +}, [isError]); +``` + +This concludes the tutorial. You should now have a basic understanding of how to add a new application to the Ledger Browser. If you have any questions, please reach out for support on the Hyperledger Discord Cacti channel. The final tutorial application can be found in `apps/tutorial-app`. + +## Future improvements + +- Extend the Data Fetch page with sub-pages to demonstrate how to use child routes. +- Implement a simple status hook and component. diff --git a/docs/docs/cactus/ledger-browser/images/supabase-credentials.png b/docs/docs/cactus/ledger-browser/images/supabase-credentials.png new file mode 100644 index 0000000000..4582aa041f Binary files /dev/null and b/docs/docs/cactus/ledger-browser/images/supabase-credentials.png differ diff --git a/docs/docs/cactus/ledger-browser/overview.md b/docs/docs/cactus/ledger-browser/overview.md new file mode 100644 index 0000000000..bee8d3df28 --- /dev/null +++ b/docs/docs/cactus/ledger-browser/overview.md @@ -0,0 +1,13 @@ +# Cacti Ledger Browser + +The Cacti Ledger Browser is a pluggable, extensible GUI for viewing and interacting with multiple Distributed Ledger Technologies (DLTs) supported by Hyperledger Cacti. Each ledger can be supported by a task-specific application that can be configured and set up dynamically. This documentation will guide you through the features, setup, and usage of the web app. + +### Features + +--- + +- **Common Development Platform**: Provides a unified platform for developing various UI applications involving ledgers supported by Hyperledger Cacti. +- **Component Reusability**: Allows components from other apps to be used, facilitating the creation of custom client applications. +- **Dynamic Configuration**: Enables dynamic app configuration to meet any user's needs. +- **Modern Technology Stack**: Built using React 18 and Vite 5 for a robust and efficient front-end experience. +- **Backend Integration**: Utilizes Supabase for backend data storage, ensuring secure and scalable data management. diff --git a/docs/docs/cactus/ledger-browser/plugin-apps/app-patterns.md b/docs/docs/cactus/ledger-browser/plugin-apps/app-patterns.md new file mode 100644 index 0000000000..07d66af784 --- /dev/null +++ b/docs/docs/cactus/ledger-browser/plugin-apps/app-patterns.md @@ -0,0 +1,14 @@ +# App Patterns + +This page presents different patterns that some plugin applications follow. While not exhaustive, this information can be useful for developing new applications or investigating existing ones. + +## Browser App / Persistence Plugin Apps + +These applications rely heavily on the existence of Cacti persistence plugins, such as [cactus-plugin-persistence-ethereum](https://github.com/hyperledger/cacti/tree/main/packages/cactus-plugin-persistence-ethereum) or [cactus-plugin-persistence-fabric](https://github.com/hyperledger/cacti/tree/main/packages/cactus-plugin-persistence-fabric). + +As the name suggests, persistence plugins store the ledger state in an SQL database (e.g., PostgreSQL managed by Supabase). They synchronize all blocks, transactions, token operations, etc., into the database for later use and push new data as it arrives. + +Browser applications are used to display this stored data. Therefore, they require a fully operational persistence plugin to function correctly. These applications are typically read-only and do not allow any changes to the ledger state. + +### Sample Setup +![Sample Persistence Plugin Setup](images/sample_arch_persistence_app.png) diff --git a/docs/docs/cactus/ledger-browser/plugin-apps/ethereum-browser.md b/docs/docs/cactus/ledger-browser/plugin-apps/ethereum-browser.md new file mode 100644 index 0000000000..da6cf761f6 --- /dev/null +++ b/docs/docs/cactus/ledger-browser/plugin-apps/ethereum-browser.md @@ -0,0 +1,95 @@ +# Ethereum Browser App + +Application for browsing ledger state stored in a database by the Cacti Ethereum Persistence Plugin. + +## Features + +- Browse ledger blocks and transactions. +- View account transaction history. +- See ERC20 and ERC721 tokens owned by a specified account. + +## Setup + +### Persistence Plugin + +#### Supabase + +The persistence plugin requires a Supabase instance to save ledger data. You can use the same Supabase instance as for the GUI (but in a separate schema), or create a separate instance specifically for this plugin. + +To set up the GUI app, you'll need a `Supabase URL`, `API key`, and the `Schema` under which the data resides in the database. + +Additionally, you'll need a `PostgreSQL connection string` to start the persistence plugin. + +##### Expose custom schema + +Fabric data is stored in a custom schema named `ethereum`. To access this data from our GUI application, you need to ensure the schema is exposed. If you're using our [supabase-all-in-one](https://github.com/hyperledger/cacti/tree/main/tools/docker/supabase-all-in-one) image, all application schemas, including `ethereum`, are already exposed, so you can skip this step. However, if you're using Supabase Cloud, you'll need to go to the [API settings](https://supabase.com/dashboard/project/_/settings/api) and add `ethereum` to the `Exposed schemas` list. + +For more details, refer to [the full guide on custom schemas](https://supabase.com/docs/guides/api/using-custom-schemas) (note that the necessary SQL code has already been executed during the persistence plugin schema setup). + +#### Ethereum Ledger (Optional) + +This step is optional as you can use any running Ethereum ledger. However, for testing purposes, you may use our [geth-all-in-one](https://github.com/hyperledger/cacti/tree/main/tools/docker/geth-all-in-one). To start it, execute the following commands from the root of your project: + +```shell +# Build +docker build ./tools/docker/geth-all-in-one/ -t cactus_geth_all_in_one + +# Run +docker run --rm --name geth_aio_testnet --detach -p 8545:8545 -p 8546:8546 cactus_geth_all_in_one +``` + +### Persistence Plugin + +Follow the instructions in the [plugin README file](https://github.com/hyperledger/cacti/tree/main/packages/cactus-plugin-persistence-ethereum). + +To quickly set up the plugin for your Ethereum ledger, run the sample setup script: + +```shell +cd packages/cactus-plugin-persistence-ethereum/ +# Replace the environment variables with JSON-RPC WS url to your ledger and postgresql connection string to your database instance. +ETHEREUM_RPC_WS_HOST=ws://127.0.0.1:8546 SUPABASE_CONNECTION_STRING=postgresql://postgres:your-super-secret-and-long-postgres-password@127.0.0.1:5432/postgres npm run sample-setup +``` + +## Configuration + +- `supabaseUrl`: URL of your Supabase instance. +- `supabaseKey`: Supabase API key. +- `supabaseSchema`: Database schema under which Ethereum persistence tables were created. + +### Sample Configuration + +Uses a localhost `supabase-all-in-one` instance with data stored in the `ethereum` schema. + +```json +{ + "supabaseUrl": "http://localhost:8000", + "supabaseKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE", + "supabaseSchema": "ethereum" +} +``` + +## Test Data Setup + +For GUI development, you don't need to start the persistence plugin. Instead, follow these steps to fill the database with sample data. + +### Create database schema tables + +- Copy the content of `packages/cactus-plugin-persistence-ethereum/src/main/sql/schema.sql` into `SQL Editor` of supabase and run the query. +- Alternatively, use `psql`: + +```shell +psql "__CONNECTION_STRING_TO_DB__" -f packages/cactus-plugin-persistence-ethereum/src/main/sql/schema.sql +``` + +### Insert sample data + +- Copy the content of `packages/cactus-plugin-persistence-ethereum/src/test/sql/insert-test-data.sql` into `SQL Editor` of supabase and run the query. +- Alternatively, use `psql`: + +```shell +psql "__CONNECTION_STRING_TO_DB__" -f packages/cactus-plugin-persistence-ethereum/src/test/sql/insert-test-data.sql +``` + +### Test Account + +- `0x06fC56347D91C6aD2dAE0c3ba38Eb12AB0D72E97` (contains some ERC20 and ERC721) diff --git a/docs/docs/cactus/ledger-browser/plugin-apps/fabric-browser.md b/docs/docs/cactus/ledger-browser/plugin-apps/fabric-browser.md new file mode 100644 index 0000000000..34f5cac733 --- /dev/null +++ b/docs/docs/cactus/ledger-browser/plugin-apps/fabric-browser.md @@ -0,0 +1,92 @@ +# Fabric Browser App + +Application for browsing ledger state stored in a database by the Cacti Fabric Persistence Plugin. + +## Features + +- Browse ledger blocks and transactions. +- View transaction details, including sender/endorser information, chaincode, and decoded method arguments. + +## Setup + +### Persistence Plugin + +#### Supabase + +The persistence plugin requires a Supabase instance to save ledger data. You can use the same Supabase instance as for the GUI (but in a separate schema), or create a separate instance specifically for this plugin. + +To set up the GUI app, you'll need a `Supabase URL`, `API key`, and the `Schema` under which the data resides in the database. + +Additionally, you'll need a `PostgreSQL connection string` to start the persistence plugin. + +##### Expose custom schema + +Fabric data is stored in a custom schema named `fabric`. To access this data from our GUI application, you need to ensure the schema is exposed. If you're using our [supabase-all-in-one](https://github.com/hyperledger/cacti/tree/main/tools/docker/supabase-all-in-one) image, all application schemas, including `fabric`, are already exposed, so you can skip this step. However, if you're using Supabase Cloud, you'll need to go to the [API settings](https://supabase.com/dashboard/project/_/settings/api) and add `fabric` to the `Exposed schemas` list. + +For more details, refer to [the full guide on custom schemas](https://supabase.com/docs/guides/api/using-custom-schemas) (note that the necessary SQL code has already been executed during the persistence plugin schema setup). + +#### Fabric Ledger (Optional) + +This step is optional as you can use any running Fabric ledger. However, for testing purposes, you may use our [fabric-all-in-one](https://github.com/hyperledger/cacti/tree/main/tools/docker/fabric-all-in-one). To start it, execute the following commands from the root of your project: + +```shell +# Build +docker build ./tools/docker/fabric-all-in-one/ -f ./tools/docker/fabric-all-in-one/Dockerfile_v2.x -t faio2x + +# Run +docker run --detach --privileged --publish-all --name faio2x-testnet faio2x +``` + +### Persistence Plugin + +Follow the instructions in the [plugin README file](https://github.com/hyperledger/cacti/tree/main/packages/cactus-plugin-persistence-fabric). + +To quickly set up the plugin for your Fabric ledger, run the complete setup script: + +```shell +cd packages/cactus-plugin-persistence-fabric/ +# Replace the environment variables with JSON-RPC WS url to your ledger and postgresql connection string to your database instance. +SUPABASE_CONNECTION_STRING=postgresql://postgres:your-super-secret-and-long-postgres-password@127.0.0.1:5432/postgres npm run complete-sample-scenario +``` + +This script will start a test ledger and push some data to it. For details on running this plugin for own ledger, see the plugin documentation (sample setup script). + +## Configuration + +- `supabaseUrl`: URL of your Supabase instance. +- `supabaseKey`: Supabase API key. +- `supabaseSchema`: Database schema under which Fabric persistence tables were created. + +### Sample Configuration + +Uses a localhost `supabase-all-in-one` instance with data stored in the `fabric` schema. + +```json +{ + "supabaseUrl": "http://localhost:8000", + "supabaseKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE", + "supabaseSchema": "fabric" +} +``` + +## Test Data Setup + +For GUI development, you don't need to start the persistence plugin. Instead, follow these steps to fill the database with sample data. + +### Create database schema tables + +- Copy the content of `packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql` into `SQL Editor` of supabase and run the query. +- Alternatively, use `psql`: + +```shell +psql "__CONNECTION_STRING_TO_DB__" -f packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql +``` + +### Insert sample data + +- Copy the content of `packages/cactus-plugin-persistence-fabric/src/test/sql/insert-test-data.sql` into `SQL Editor` of supabase and run the query. +- Alternatively, use `psql`: + +```shell +psql "__CONNECTION_STRING_TO_DB__" -f packages/cactus-plugin-persistence-fabric/src/test/sql/insert-test-data.sql +``` diff --git a/docs/docs/cactus/ledger-browser/plugin-apps/images/sample_arch_persistence_app.png b/docs/docs/cactus/ledger-browser/plugin-apps/images/sample_arch_persistence_app.png new file mode 100644 index 0000000000..ede0bb5d87 Binary files /dev/null and b/docs/docs/cactus/ledger-browser/plugin-apps/images/sample_arch_persistence_app.png differ diff --git a/docs/docs/cactus/ledger-browser/setup.md b/docs/docs/cactus/ledger-browser/setup.md new file mode 100644 index 0000000000..0f8729909a --- /dev/null +++ b/docs/docs/cactus/ledger-browser/setup.md @@ -0,0 +1,175 @@ +# Setup + +## Clone the repository + +Clone the [cacti git repository](https://github.com/hyperledger/cacti) on your local machine. Follow these instructions to get a copy of the project up and running for development and testing purposes. + +```sh +git clone https://github.com/hyperledger/cacti.git +cd cacti/packages/cacti-ledger-browser/ +``` + +## App Prerequisites + +### Install Node + +NodeJS v18 or later is required (we recommend using the Node Version Manager (nvm) if available for your OS). + +```sh +nvm install 18 +nvm use 18 +``` + +### Install Yarn + +Run the following command from within the project directory: + +```sh +npm run enable-corepack +``` + +### Install Docker + +Ensure Docker Engine is installed and running. You can verify this by running `docker ps -aq`. Follow the installation instructions for Docker [here](https://docs.docker.com/engine/install/ubuntu/). + +### Install required packages + +You can run the following command from either the root of the project or the `cacti-ledger-browser` folder: + +```sh +yarn install +``` + +## Setup Supabase + +### Create Instance + +You can start a local instance of Supabase to be used as the GUI backend using our Cacti `supabase-all-in-one` image. Alternatively, follow the [Supabase Self-Hosting guide](https://supabase.com/docs/guides/self-hosting/docker). + +#### Using Self-Hosting + +1. Open a new console window in the root of the Cacti project. +2. Build the `cactus-supabase-all-in-one` image: + + ```sh + docker build ./tools/docker/supabase-all-in-one -t cactus-supabase-all-in-one + ``` + +3. Run the container (detached): + + ```sh + docker run --name supabase_all_in_one_gui \ + --detach \ + --privileged \ + -p 8000:8000 \ + -p 5432:5432 \ + cactus-supabase-all-in-one + ``` + +4. Copy and save the `SERVICE_ROLE_KEY` (our `API Key`) from the Supabase environment, it will be needed later: + + ```sh + docker exec -ti supabase_all_in_one_gui cat /home/supabase/docker/.env | grep SERVICE_ROLE_KEY + ``` + +5. Open the Supabase dashboard by navigating to http://localhost:8000/ +6. Use the following credentials to log in: + + - **Username**: `supabase` + - **Password**: `this_password_is_insecure_and_should_be_updated` + +#### Using Supabase Cloud + +Supabase provides a free tier that can be used for development and small projects. + +1. Open the dashboard https://supabase.com/dashboard/projects +1. You'll be asked to log in or sign up if you haven't done that already. You can also use your GitHub account to quickly create the account. +1. After a successful login, you'll see a list of your projects (possibly empty). Click the `New project` button on the top of the page to create a new project. Select a default organization (or create a new one if needed). +1. Set any project name and secure database password (remember or save it, it will be needed when setting up some plugin apps). Select a region close to your location. +1. Wait for the project to set up. +1. You can see connection details on the new project home page. Save the `Project URL` and `API Key`, they will be used later on. + +![Supabase Cloud credentials window](images/supabase-credentials.png) + +### Setup DB Schema + +1. Navigate to `SQL Editor` using the navigation panel on the left. Copy the content of `packages/cacti-ledger-browser/src/main/sql/schema.sql` into the editor and run the query (`Run` button or `Ctrl + Enter`). +1. Navigate to `Table Editor` and verify that tables were created in the `public` schema. + +Alternatively, you can use the `psql` CLI tool: + +```shell +psql "__CONNECTION_STRING_TO_DB__" -f packages/cacti-ledger-browser/src/main/sql/schema.sql +``` + +## Start the application + +### Set the environment variables + +1. Copy `packages/cacti-ledger-browser/.env.template` to `packages/cacti-ledger-browser/.env`. +2. Edit the newly created `.env` file (`vim packages/cacti-ledger-browser/.env` or similar). +3. Set `VITE_SUPABASE_URL` to: + - **Self Hosting**: `http://localhost:8000`. + - **Supabase Cloud**: `Project URL` from the config page. +4. Set `VITE_SUPABASE_KEY` to: + - **Self Hosting**: `SERVICE_ROLE_KEY` from `.env` file withing the container. + - **Supabase Cloud**: `API Key` from the config page. +5. Leave `VITE_SUPABASE_SCHEMA` as `public`. + +If there are any connection errors to Supabase when running the app, double-check if the data in this file was filled correctly! + +### Development build + +```sh +yarn run start +``` + +The server will run on http://localhost:3001/ + +### Production build + +```sh +yarn run build +yarn run serve +``` + +The server will run on http://localhost:4173/ + +## Use application + +By default, there will be no configured apps. Click the `Add Application` action card to open the wizard that will guide you through the process. + +### Adding Applications + +#### Select Group + +All (pluggable) applications are divided into groups for better organization. + +#### Select Application + +Select the application you want to add. Use the `Setup Guide` button to the right of a given app to open its documentation in a separate card. If there's no documentation for a given plugin, the button is grayed out. + +#### Common Setup + +These are fields common for all plugin apps. + +- **Instance Name**: Used to uniquely identify the given application. +- **Description**: Provides more context to the app user. +- **Path**: Path under which the application routes will be mounted. Example: an app with path `/eth` will be available under `http://localhost:3001/eth/`. **Must be unique!** + +#### App Specific Setup + +- Each app may require its own configuration (e.g., access to the database where data is stored, authentication keys, Cacti connector endpoint, etc.). +- These options can be configured using JSON format. +- Each app can define its own custom format, so this will vary from app to app. Consult the application documentation for more details. + +### App Dashboard + +On the main page, you can see the list of currently configured applications. Each app has two health indicators: + +- **Initialized**: Indicates whether the plugin and all infrastructure required by it were created successfully. +- **Status**: Indicates if all components are running. + +You can open the detailed status page by clicking the `Status` button on the app card (this may vary between different plugin apps). You can open the settings page by clicking on the `Configure` button. Clicking on the card itself will navigate to the plugin application. + +At any time, you can navigate back to this page by clicking the app grid icon on the far left of the top navigation bar. You can open the documentation for a given app by clicking the far right question mark button on the navigation bar. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b365ccd1b2..0cf4f603d3 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -114,6 +114,17 @@ nav: - Test Api Client: cactus/packages/cactus-test-api-client.md - Test CMD Api Server: cactus/packages/cactus-test-cmd-api-server.md - Test Tooling: cactus/packages/cactus-test-tooling.md + - Ledger Browser: + - Overview: cactus/ledger-browser/overview.md + - Setup: cactus/ledger-browser/setup.md + - Plugin Apps: + - App Patterns: cactus/ledger-browser/plugin-apps/app-patterns.md + - Ethereum Browser: cactus/ledger-browser/plugin-apps/ethereum-browser.md + - Hyperledger Fabric Browser: cactus/ledger-browser/plugin-apps/fabric-browser.md + - Developer Guide: + - Overview: cactus/ledger-browser/developer-guide/overview.md + - Architecture: cactus/ledger-browser/developer-guide/architecture.md + - Tutorial: cactus/ledger-browser/developer-guide/tutorial.md - Ledger Support: - Overview: cactus/support.md - Hyperledger Besu: cactus/support/besu.md diff --git a/packages/cacti-ledger-browser/package.json b/packages/cacti-ledger-browser/package.json index 5c1a6999ae..57ea068267 100644 --- a/packages/cacti-ledger-browser/package.json +++ b/packages/cacti-ledger-browser/package.json @@ -1,6 +1,7 @@ { "name": "@hyperledger/cacti-ledger-browser", "version": "2.0.0-rc.3", + "private": true, "description": "Cacti GUI for visualizing ledger data build on react.", "keywords": [ "Hyperledger", @@ -41,11 +42,12 @@ } ], "scripts": { - "build": "yarn run build:prod:frontend", + "build": "yarn run tsc && yarn run build:prod:frontend", "build:dev:frontend": "vite build --mode=development", "build:prod:frontend": "vite build", "serve": "vite preview", - "start": "vite" + "start": "vite", + "tsc": "tsc --build --verbose" }, "dependencies": { "@emotion/react": "11.11.4", @@ -56,6 +58,7 @@ "@supabase/supabase-js": "1.35.6", "@tanstack/react-query": "5.29.2", "apexcharts": "3.45.2", + "axios": "1.7.2", "buffer": "6.0.3", "ethers": "6.12.1", "react": "18.2.0", @@ -72,5 +75,12 @@ "@vitejs/plugin-react": "4.2.1", "typescript": "5.5.2", "vite": "5.1.7" + }, + "engines": { + "node": ">=18", + "npm": ">=8" + }, + "publishConfig": { + "access": "public" } } diff --git a/packages/cacti-ledger-browser/src/main/sql/schema.sql b/packages/cacti-ledger-browser/src/main/sql/schema.sql new file mode 100644 index 0000000000..03915a4276 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/sql/schema.sql @@ -0,0 +1,52 @@ +ALTER SCHEMA extensions OWNER TO postgres; +ALTER SCHEMA public OWNER TO postgres; + +-- Table: public.plugin_status +-- DROP TABLE IF EXISTS public.plugin_status; + +CREATE TABLE IF NOT EXISTS public.plugin_status +( + name text COLLATE pg_catalog."default" NOT NULL, + last_instance_id text COLLATE pg_catalog."default" NOT NULL, + is_schema_initialized boolean NOT NULL, + created_at timestamp with time zone NOT NULL DEFAULT now(), + last_connected_at timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT plugin_status_pkey PRIMARY KEY (name), + CONSTRAINT plugin_status_name_key UNIQUE (name) +) + +TABLESPACE pg_default; + +ALTER TABLE IF EXISTS public.plugin_status + OWNER to postgres; + +GRANT ALL ON TABLE public.plugin_status TO anon; +GRANT ALL ON TABLE public.plugin_status TO authenticated; +GRANT ALL ON TABLE public.plugin_status TO postgres; +GRANT ALL ON TABLE public.plugin_status TO service_role; + + +-- Table: public.gui_app_config +-- DROP TABLE IF EXISTS public.gui_app_config; + +CREATE TABLE public.gui_app_config ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + app_id text NOT NULL, + instance_name text NOT NULL, + description text NOT NULL, + path text NOT NULL, + options json, + created_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT gui_app_config_pkey PRIMARY KEY (id), + CONSTRAINT gui_app_config_path_key UNIQUE (path) +) + +TABLESPACE pg_default; + +ALTER TABLE IF EXISTS public.gui_app_config + OWNER to postgres; + +GRANT ALL ON TABLE public.gui_app_config TO anon; +GRANT ALL ON TABLE public.gui_app_config TO authenticated; +GRANT ALL ON TABLE public.gui_app_config TO postgres; +GRANT ALL ON TABLE public.gui_app_config TO service_role; diff --git a/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx b/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx index 2be409ec8e..7923d037d4 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx @@ -18,47 +18,26 @@ import { themeOptions } from "./theme"; import ContentLayout from "./components/Layout/ContentLayout"; import HeaderBar from "./components/Layout/HeaderBar"; import HomePage from "./pages/home/HomePage"; -import { AppInstance, AppListEntry } from "./common/types/app"; +import { AppInstance } from "./common/types/app"; import { patchAppRoutePath } from "./common/utils"; import { NotificationProvider } from "./common/context/NotificationContext"; import { guiAppConfig } from "./common/queries"; import createApplications from "./common/createApplications"; import ConnectionFailedDialog from "./components/ConnectionFailedDialog/ConnectionFailedDialog"; -/** - * Get list of all apps from the config - */ -function getAppList(appConfig: AppInstance[]) { - const appList: AppListEntry[] = appConfig.map((app) => { - return { - path: app.path, - name: app.appName, - }; - }); - - appList.unshift({ - path: "/", - name: "Home", - }); - - return appList; -} - /** * Create header bar for each app based on app menuEntries field in config. */ function getHeaderBarRoutes(appConfig: AppInstance[]) { - const appList = getAppList(appConfig); - const headerRoutesConfig = appConfig.map((app) => { return { key: app.path, path: `${app.path}/*`, element: ( ), }; @@ -66,7 +45,9 @@ function getHeaderBarRoutes(appConfig: AppInstance[]) { headerRoutesConfig.push({ key: "home", path: `*`, - element: , + element: ( + + ), }); return useRoutes(headerRoutesConfig); } diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20BalanceHistoryChart.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20BalanceHistoryChart.tsx index ab092df29e..b6a537cac6 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20BalanceHistoryChart.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20BalanceHistoryChart.tsx @@ -57,7 +57,7 @@ export default function ERC20BalanceHistoryChart({ colors: [theme.palette.primary.main], xaxis: { type: "datetime", - categories: data?.map((txn) => new Date(txn.created_at).getTime()), + categories: data?.map((txn) => txn.created_at), labels: { format: "dd-MM-yyyy h:mm", }, diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx index 78fd712e40..1a21893187 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx @@ -42,7 +42,7 @@ const ethBrowserAppDefinition: AppDefinition = { return { id: app.id, - appName: "Ethereum Browser", + appName: ethBrowserAppDefinition.appName, instanceName: app.instance_name, description: app.description, path: app.path, @@ -78,6 +78,8 @@ const ethBrowserAppDefinition: AppDefinition = { StatusComponent: ( ), + appSetupGuideURL: ethBrowserAppDefinition.appSetupGuideURL, + appDocumentationURL: ethBrowserAppDefinition.appDocumentationURL, }; }, }; 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 7f4ede78fa..f2c30200e4 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 @@ -44,7 +44,7 @@ const fabricBrowserAppDefinition: AppDefinition = { return { id: app.id, - appName: "Hyperledger Fabric Browser", + appName: fabricBrowserAppDefinition.appName, instanceName: app.instance_name, description: app.description, path: app.path, @@ -86,6 +86,8 @@ const fabricBrowserAppDefinition: AppDefinition = { StatusComponent: ( ), + appSetupGuideURL: fabricBrowserAppDefinition.appSetupGuideURL, + appDocumentationURL: fabricBrowserAppDefinition.appDocumentationURL, }; }, }; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/hooks.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/hooks.tsx new file mode 100644 index 0000000000..b714847175 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/hooks.tsx @@ -0,0 +1,9 @@ +import { useOutletContext } from "react-router-dom"; + +type TutorialOptionsType = { + name: string; +}; + +export function useAppOptions() { + return useOutletContext(); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/index.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/index.tsx new file mode 100644 index 0000000000..7fe747cab6 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/index.tsx @@ -0,0 +1,72 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { AppDefinition } from "../../common/types/app"; +import { GuiAppConfig } from "../../common/supabase-types"; +import { AppCategory } from "../../common/app-category"; +import Home from "./pages/Home"; +import DataFetch from "./pages/DataFetch"; + +const tutorialAppDefinition: AppDefinition = { + appName: "Tutorial App", + category: AppCategory.SampleApp, + defaultInstanceName: "My App", + defaultDescription: "This is a tutorial application.", + defaultPath: "/tutorial", + defaultOptions: { + // Our app will use this variable to display a greeting later on + name: "Cacti", + }, + + createAppInstance(app: GuiAppConfig) { + if (!app.options || !app.options.name) { + throw new Error(`Missing 'name' in received GuiAppConfig options!`); + } + + return { + id: app.id, + appName: tutorialAppDefinition.appName, + instanceName: app.instance_name, + description: app.description, + path: app.path, + options: app.options, + menuEntries: [ + { + title: "Home", + url: "/", + }, + { + title: "Data Fetch", + url: "/data-fetch", + }, + ], + routes: [ + { + element: , + }, + { + path: "data-fetch", + element: , + }, + ], + useAppStatus: () => { + return { + isPending: false, + isInitialized: true, + status: { + severity: "success", + message: "Mocked response!", + }, + }; + }, + StatusComponent: ( + + Everything is OK (we hope) + + ), + appSetupGuideURL: tutorialAppDefinition.appSetupGuideURL, + appDocumentationURL: tutorialAppDefinition.appDocumentationURL, + }; + }, +}; + +export default tutorialAppDefinition; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/pages/DataFetch.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/pages/DataFetch.tsx new file mode 100644 index 0000000000..49b47fc0d2 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/pages/DataFetch.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import axios from "axios"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useQuery } from "@tanstack/react-query"; +import PageTitle from "../../../components/ui/PageTitle"; +import { useNotification } from "../../../common/context/NotificationContext"; + +/** + * Simple method for fetching test data from restful-api.dev + */ +async function fetchSampleData() { + const response = await axios.get("https://api.restful-api.dev/objects/7"); + return response.data; +} + +export default function DataFetch() { + const { showNotification } = useNotification(); + const { data, isPending, isError, error } = useQuery({ + queryKey: ["sampleFetch"], + queryFn: fetchSampleData, + }); + + React.useEffect(() => { + isError && + showNotification(`Could not fetch sample data: ${error}`, "error"); + }, [isError]); + + React.useEffect(() => { + !isPending && + data && + showNotification(`Fetched data: ${data.name}`, "success"); + }, [data, isPending]); + + return ( + + Data Fetch Sample + + + Fetched object: {data?.name ?? ""} + + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/pages/Home.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/pages/Home.tsx new file mode 100644 index 0000000000..27c5df281b --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/tutorial-app/pages/Home.tsx @@ -0,0 +1,13 @@ +import Box from "@mui/material/Box"; +import PageTitle from "../../../components/ui/PageTitle"; +import { useAppOptions } from "../hooks"; + +export default function Home() { + const appOptions = useAppOptions(); + + return ( + + Hello {appOptions.name}! + + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/config.tsx b/packages/cacti-ledger-browser/src/main/typescript/common/config.tsx index 983a8cbcac..28c633a61a 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/common/config.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/common/config.tsx @@ -1,10 +1,12 @@ import ethBrowserAppDefinition from "../apps/eth"; import fabricBrowserAppDefinition from "../apps/fabric"; +import tutorialAppDefinition from "../apps/tutorial-app"; import { AppDefinition } from "./types/app"; const config = new Map([ ["ethereumPersistenceBrowser", ethBrowserAppDefinition], ["fabricPersistenceBrowser", fabricBrowserAppDefinition], + ["tutorialApplication", tutorialAppDefinition], ]); export default config; diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/types/app.ts b/packages/cacti-ledger-browser/src/main/typescript/common/types/app.ts index 7da191ab78..136850ad37 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/common/types/app.ts +++ b/packages/cacti-ledger-browser/src/main/typescript/common/types/app.ts @@ -30,27 +30,114 @@ export interface AppInstancePersistencePluginOptions { } export interface AppInstance { + /** + * Unique database ID of this app instance. + */ id: string; + + /** + * Name of the application (can be same as appName in app definition) + */ appName: string; + + /** + * Instance name (set by the user) + */ instanceName: string; + + /** + * Instance description (set by the user) + */ description: string | undefined; + + /** + * Path under which app routes will be mounted (must be path with `/`, like `/eth`) + */ path: string; + + /** + * Custom, app-specific options in JSON format. This will change between applications. + */ options: T; + + /** + * List on titles and URL of menu entries to be added to the top bar (used to navigate within an app) + */ menuEntries: AppInstanceMenuEntry[]; + + /** + * `react-router-dom` compatible list of this application routes. + */ routes: RouteObject[]; + + /** + * Method for retriving application status details. + */ useAppStatus: () => GetStatusResponse; + + /** + * Status component showed when user opens a app status pop up window. + */ StatusComponent: React.ReactElement; + + /** + * Full URL to a setup guide, it will be displayed to the user on app configuration page. + */ + appSetupGuideURL?: string; + + /** + * Full URL to app documentation page + */ + appDocumentationURL?: string; } export type CreateAppInstanceFactoryType = (app: GuiAppConfig) => AppInstance; export interface AppDefinition { + /** + * Application name as shown to the user + */ appName: string; + + /** + * Application category, the user can filter using it. + * If there's no matching category for your app consider adding a new one! + */ category: string; + + /** + * Full URL to a setup guide, it will be displayed to the user on app configuration page. + */ + appSetupGuideURL?: string; + + /** + * Full URL to app documentation page + */ + appDocumentationURL?: string; + + /** + * Default value for instance name that user can set to uniquely identify this ap instance. + */ defaultInstanceName: string; + + /** + * Default value for app description. + */ defaultDescription: string; + + /** + * Default path under which app routes will be mounted (must be path with `/`, like `/eth`) + */ defaultPath: string; + + /** + * Default custom, app-specific options in JSON format. This will change between applications. + */ defaultOptions: unknown; + + /** + * Factory method for creating application instance object from configuration stored in a database. + */ createAppInstance: CreateAppInstanceFactoryType; } diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/utils.ts b/packages/cacti-ledger-browser/src/main/typescript/common/utils.ts index 109187a799..6a2d38b171 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/common/utils.ts +++ b/packages/cacti-ledger-browser/src/main/typescript/common/utils.ts @@ -15,3 +15,21 @@ export function patchAppRoutePath(appPath: string, routePath?: string) { return appPath; } } + +/** + * Returns true if provided url is defined and valid, returns false and + * writes error to `console.error` otherwise. + */ +export function isValidUrl(urlString?: string) { + if (!urlString) { + return false; + } + + try { + new URL(urlString); + return true; + } catch (e) { + console.error(`Invalid URL provided: ${urlString}, error: ${e}`); + return false; + } +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/AppSetupForms/AppSetupForm.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/AppSetupForms/AppSetupForm.tsx index a93788bed5..38cf082aa6 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/components/AppSetupForms/AppSetupForm.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/components/AppSetupForms/AppSetupForm.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import Box from "@mui/material/Box"; import TextField from "@mui/material/TextField"; +const pathCheckRegex = /^\/[a-zA-Z][a-zA-Z0-9]*$/; const emptyFormHelperText = "Field can't be empty"; const regularPathHelperText = "Path under which the plugin will be available, must be unique withing GUI."; @@ -30,9 +31,7 @@ export default function AppSetupForm({ const isInstanceNameEmptyError = !!!commonSetupValues.instanceName; const isDescriptionEmptyError = !!!commonSetupValues.description; const isPathEmptyError = !!!commonSetupValues.path; - const isPathInvalidError = !( - commonSetupValues.path.startsWith("/") && commonSetupValues.path.length > 1 - ); + const isPathInvalidError = !pathCheckRegex.test(commonSetupValues.path); let pathHelperText = regularPathHelperText; if (isPathEmptyError) { pathHelperText = emptyFormHelperText; diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/Layout/HeaderBar.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/Layout/HeaderBar.tsx index 14b1b6c4d9..513ce600fe 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/components/Layout/HeaderBar.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/components/Layout/HeaderBar.tsx @@ -5,18 +5,23 @@ import Box from "@mui/material/Box"; import Toolbar from "@mui/material/Toolbar"; import IconButton from "@mui/material/IconButton"; import AppsIcon from "@mui/icons-material/Apps"; +import HelpIcon from "@mui/icons-material/Help"; import Button from "@mui/material/Button"; import Tooltip from "@mui/material/Tooltip"; -import { AppInstanceMenuEntry, AppListEntry } from "../../common/types/app"; -import { patchAppRoutePath } from "../../common/utils"; +import { AppInstanceMenuEntry } from "../../common/types/app"; +import { isValidUrl, patchAppRoutePath } from "../../common/utils"; type HeaderBarProps = { - appList: AppListEntry[]; path?: string; menuEntries?: AppInstanceMenuEntry[]; + appDocumentationURL?: string; }; -const HeaderBar: React.FC = ({ path, menuEntries }) => { +export default function HeaderBarProps({ + path, + menuEntries, + appDocumentationURL, +}: HeaderBarProps) { return ( @@ -48,9 +53,19 @@ const HeaderBar: React.FC = ({ path, menuEntries }) => { ))} )} + + {isValidUrl(appDocumentationURL) && ( + window.open(appDocumentationURL, "_blank")} + > + + + )} ); -}; - -export default HeaderBar; +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/SelectAppView.tsx b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/SelectAppView.tsx index 42663bed25..94cefbd85a 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/SelectAppView.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/SelectAppView.tsx @@ -6,9 +6,11 @@ import ListItemText from "@mui/material/ListItemText"; import ListItemAvatar from "@mui/material/ListItemAvatar"; import Avatar from "@mui/material/Avatar"; import ListItemButton from "@mui/material/ListItemButton"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import config from "../../common/config"; import { AppCategory, getAppCategoryConfig } from "../../common/app-category"; +import { isValidUrl } from "../../common/utils"; export interface SelectAppViewProps { appCategory: string; @@ -36,15 +38,26 @@ export default function SelectAppView({ {apps.map(([appId, app]) => { return ( - handleAppSelected(appId)}> - - {categoryConfig.icon} - - - + + handleAppSelected(appId)}> + + {categoryConfig.icon} + + + + + ); })} diff --git a/packages/cacti-ledger-browser/tsconfig.json b/packages/cacti-ledger-browser/tsconfig.json index 6fdc580da1..bacee55f67 100644 --- a/packages/cacti-ledger-browser/tsconfig.json +++ b/packages/cacti-ledger-browser/tsconfig.json @@ -10,6 +10,7 @@ "skipLibCheck": true, "noEmit": true, "jsx": "react-jsx", + "tsBuildInfoFile": "../../.build-cache/cacti-ledger-browser.tsbuildinfo", "plugins": [ { "name": "typescript-plugin-css-modules" diff --git a/packages/cactus-plugin-persistence-ethereum/src/main/sql/schema.sql b/packages/cactus-plugin-persistence-ethereum/src/main/sql/schema.sql index 206c5120da..85fea39df9 100644 --- a/packages/cactus-plugin-persistence-ethereum/src/main/sql/schema.sql +++ b/packages/cactus-plugin-persistence-ethereum/src/main/sql/schema.sql @@ -359,3 +359,11 @@ ALTER FUNCTION ethereum.get_missing_blocks_in_range(integer, integer) OWNER TO postgres; GRANT EXECUTE ON PROCEDURE ethereum.update_issued_erc721_tokens(numeric) TO public; + +GRANT USAGE ON SCHEMA ethereum TO anon, authenticated, service_role; +GRANT ALL ON ALL TABLES IN SCHEMA ethereum TO anon, authenticated, service_role; +GRANT ALL ON ALL ROUTINES IN SCHEMA ethereum TO anon, authenticated, service_role; +GRANT ALL ON ALL SEQUENCES IN SCHEMA ethereum TO anon, authenticated, service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA ethereum GRANT ALL ON TABLES TO anon, authenticated, service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA ethereum GRANT ALL ON ROUTINES TO anon, authenticated, service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA ethereum GRANT ALL ON SEQUENCES TO anon, authenticated, service_role; diff --git a/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/common-setup-methods.ts b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/common-setup-methods.ts index 24d8790caa..1eb2307f4d 100644 --- a/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/common-setup-methods.ts +++ b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/common-setup-methods.ts @@ -102,6 +102,8 @@ export async function setupApiServer(port: number, rpcApiWsHost: string) { cactusApiServerOptions.apiCorsDomainCsv = "*"; cactusApiServerOptions.apiTlsEnabled = false; cactusApiServerOptions.apiPort = port; + cactusApiServerOptions.grpcPort = port + 1; + cactusApiServerOptions.crpcPort = port + 2; const config = await configService.newExampleConfigConvict( cactusApiServerOptions, ); diff --git a/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/complete-sample-scenario.ts b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/complete-sample-scenario.ts index 6777699ba1..5393d65e12 100644 --- a/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/complete-sample-scenario.ts +++ b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/complete-sample-scenario.ts @@ -207,7 +207,7 @@ async function main() { // Set up the ApiServer with Ethereum Connector and Ethereum Persistence plugins. // It returns the persistence plugin, which we can use to run monitoring operations. - const persistence = await setupApiServer(9782, rpcApiWsHost); + const persistence = await setupApiServer(9530, rpcApiWsHost); console.log("Environment is running..."); // Deploy an ERC721 contract to our test ledger and mint some tokens, diff --git a/packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql b/packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql index 8b59c85e43..56930d05aa 100644 --- a/packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql +++ b/packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql @@ -223,3 +223,11 @@ ALTER TABLE ONLY fabric.transaction ALTER TABLE ONLY fabric.transaction ADD CONSTRAINT transaction_block_number_fkey FOREIGN KEY (block_number) REFERENCES fabric.block(number); + +GRANT USAGE ON SCHEMA fabric TO anon, authenticated, service_role; +GRANT ALL ON ALL TABLES IN SCHEMA fabric TO anon, authenticated, service_role; +GRANT ALL ON ALL ROUTINES IN SCHEMA fabric TO anon, authenticated, service_role; +GRANT ALL ON ALL SEQUENCES IN SCHEMA fabric TO anon, authenticated, service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA fabric GRANT ALL ON TABLES TO anon, authenticated, service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA fabric GRANT ALL ON ROUTINES TO anon, authenticated, service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA fabric GRANT ALL ON SEQUENCES TO anon, authenticated, service_role; diff --git a/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/common-setup-methods.ts b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/common-setup-methods.ts index eabbfe999a..2432e12bcc 100644 --- a/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/common-setup-methods.ts +++ b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/common-setup-methods.ts @@ -142,6 +142,8 @@ export async function setupApiServer( cactusApiServerOptions.apiCorsDomainCsv = "*"; cactusApiServerOptions.apiTlsEnabled = false; cactusApiServerOptions.apiPort = port; + cactusApiServerOptions.grpcPort = port + 1; + cactusApiServerOptions.crpcPort = port + 2; const config = await configService.newExampleConfigConvict( cactusApiServerOptions, ); diff --git a/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/complete-sample-scenario.ts b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/complete-sample-scenario.ts index 78a582fdac..56cb9c84a9 100644 --- a/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/complete-sample-scenario.ts +++ b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/complete-sample-scenario.ts @@ -146,7 +146,7 @@ async function main() { // Set up the ApiServer with Fabric Connector and Fabric Persistence plugins. // It returns the persistence plugin, which we can use to run monitoring operations. const { persistence, apiClient, signingCredential } = await setupApiServer( - 9781, // run at that port + 9950, // run at that port ledgerChannelName, connectionProfile, userIdentity, diff --git a/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/sample-setup.ts b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/sample-setup.ts index 28aa4029e8..143a74b81f 100644 --- a/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/sample-setup.ts +++ b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/sample-setup.ts @@ -69,7 +69,7 @@ async function main() { // Set up the ApiServer with Fabric Connector and Fabric Persistence plugins. // It returns the persistence plugin, which we can use to run monitoring operations. const { persistence } = await setupApiServer( - 9781, // run at that port + 9930, // run at that port FABRIC_CHANNEL_NAME, connectionProfile, userIdentity as any, diff --git a/tools/docker/supabase-all-in-one/Dockerfile b/tools/docker/supabase-all-in-one/Dockerfile index 9d968dc8b1..955b2ba879 100644 --- a/tools/docker/supabase-all-in-one/Dockerfile +++ b/tools/docker/supabase-all-in-one/Dockerfile @@ -37,6 +37,7 @@ FROM docker:25.0.5-dind ARG freeze_tmp_dir ENV FREEZE_TMP_DIR=$freeze_tmp_dir ENV APP_ROOT="/home/supabase/docker" +ENV PGRST_DB_SCHEMAS="public, storage, graphql_public, fabric, ethereum" # Install package dependencies RUN apk update \ diff --git a/tools/docker/supabase-all-in-one/README.md b/tools/docker/supabase-all-in-one/README.md index 32f6799fa2..1bceb5c3f3 100644 --- a/tools/docker/supabase-all-in-one/README.md +++ b/tools/docker/supabase-all-in-one/README.md @@ -5,17 +5,7 @@ An all in one supabase image that can be used as Cactus GUI backend. - This docker image is for `testing` and `development` only. - **Do NOT use in production!** -## Usage - -### Dashboard credentials: - -- http://127.0.0.1:8000/ -- Username: supabase -- Password: this_password_is_insecure_and_should_be_updated - -### Postgress ccredentials: - -- Password: `your-super-secret-and-long-postgres-password` +## Running ### Docker Compose @@ -45,3 +35,29 @@ docker run --name supabase_all_in_one_gui \ -p 5432:5432 \ cactus-supabase-all-in-one ``` + +## Usage + +Supabase dashboard is available under http://localhost:8000/. Use the following credentials to access it: +- **Username**: `supabase` +- **Password**: `this_password_is_insecure_and_should_be_updated` + +### Postgres access + +Use the `psql` tool, database password is: `your-super-secret-and-long-postgres-password` + +```sh +psql -h 127.0.0.1 -p 5432 -d postgres -U postgres +``` + +#### Connection string + +```sh +postgresql://postgres:your-super-secret-and-long-postgres-password@127.0.0.1:5432/postgres +``` + +### API Key + +```sh +docker exec -ti supabase_all_in_one_gui cat /home/supabase/docker/.env | grep SERVICE_ROLE_KEY +``` diff --git a/yarn.lock b/yarn.lock index 1bd0cf1679..35024b0af3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9075,6 +9075,7 @@ __metadata: "@types/react-dom": "npm:18.2.17" "@vitejs/plugin-react": "npm:4.2.1" apexcharts: "npm:3.45.2" + axios: "npm:1.7.2" buffer: "npm:6.0.3" ethers: "npm:6.12.1" react: "npm:18.2.0"