= ({ appConfig }) => {
+function App() {
+ const { isError, isPending, data } = useQuery(guiAppConfig());
+
+ if (isError) {
+ return ;
+ }
+
+ const appConfig = createApplications(data);
+
const headerRoutes = getHeaderBarRoutes(appConfig);
const contentRoutes = getContentRoutes(appConfig);
return (
+ {isPending && (
+
+ )}
{headerRoutes}
{contentRoutes}
);
-};
+}
// MUI Theme
const theme = createTheme(themeOptions);
@@ -114,20 +142,18 @@ const theme = createTheme(themeOptions);
// React Query client
const queryClient = new QueryClient();
-const CactiLedgerBrowserApp: React.FC = ({ appConfig }) => {
+export default function CactiLedgerBrowserApp() {
return (
-
+
{/* */}
);
-};
-
-export default CactiLedgerBrowserApp;
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/AccountERC20View.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/AccountERC20View.tsx
index 5d052afcd8..8d7eb3ff6d 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/AccountERC20View.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/AccountERC20View.tsx
@@ -3,9 +3,9 @@ import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
-import { TokenERC20 } from "../../../../common/supabase-types";
import ERC20TokenList from "./ERC20TokenList";
import ERC20TokenDetails from "./ERC20TokenDetails";
+import { TokenERC20 } from "../../supabase-types";
export type AccountERC20ViewProps = {
accountAddress: string;
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20BalanceHistoryTable.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20BalanceHistoryTable.tsx
index 730c5afd41..fc935b9e0a 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20BalanceHistoryTable.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20BalanceHistoryTable.tsx
@@ -14,8 +14,8 @@ import Typography from "@mui/material/Typography";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
-import { TokenHistoryItem20 } from "../../../../common/supabase-types";
import ShortenedTypography from "../../../../components/ui/ShortenedTypography";
+import { TokenHistoryItem20 } from "../../supabase-types";
const StyledHeaderCell = styled(TableCell)(({ theme }) => ({
color: theme.palette.primary.main,
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20TokenDetails.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20TokenDetails.tsx
index 184cf3ba82..e1283f5d4d 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20TokenDetails.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20TokenDetails.tsx
@@ -7,12 +7,12 @@ import Typography from "@mui/material/Typography";
import Skeleton from "@mui/material/Skeleton";
import { ethERC20TokenHistory } from "../../queries";
-import { TokenERC20 } from "../../../../common/supabase-types";
import ShortenedTypography from "../../../../components/ui/ShortenedTypography";
import { useNotification } from "../../../../common/context/NotificationContext";
import ERC20BalanceHistoryChart from "./ERC20BalanceHistoryChart";
import ERC20BalanceHistoryTable from "./ERC20BalanceHistoryTable";
import { createBalanceHistoryList } from "./balanceHistory";
+import { TokenERC20 } from "../../supabase-types";
function TokenDetailsPlaceholder() {
return (
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20TokenList.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20TokenList.tsx
index 50208a7563..e872e59c8b 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20TokenList.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/ERC20TokenList.tsx
@@ -14,8 +14,8 @@ import TableHead from "@mui/material/TableHead";
import CircularProgress from "@mui/material/CircularProgress";
import { useNotification } from "../../../../common/context/NotificationContext";
-import { TokenERC20 } from "../../../../common/supabase-types";
import { ethAllERC20TokensByAccount } from "../../queries";
+import { TokenERC20 } from "../../supabase-types";
const StyledHeaderCell = styled(TableCell)(({ theme }) => ({
color: theme.palette.primary.main,
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/balanceHistory.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/balanceHistory.ts
index 36e62f745a..c48a4f7abe 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/balanceHistory.ts
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/AccountERC20View/balanceHistory.ts
@@ -1,4 +1,4 @@
-import { TokenHistoryItem20 } from "../../../../common/supabase-types";
+import { TokenHistoryItem20 } from "../../supabase-types";
export type BalanceHistoryListData = {
created_at: string;
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/hooks.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/hooks.tsx
new file mode 100644
index 0000000000..fb6ffde86d
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/hooks.tsx
@@ -0,0 +1,16 @@
+import { useOutletContext } from "react-router-dom";
+import { AppInstancePersistencePluginOptions } from "../../common/types/app";
+
+export function useEthAppConfig() {
+ return useOutletContext();
+}
+
+export function useEthSupabaseConfig() {
+ const appConfig = useEthAppConfig();
+
+ return {
+ schema: appConfig.supabaseSchema,
+ url: appConfig.supabaseUrl,
+ key: appConfig.supabaseKey,
+ };
+}
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 9ee731b528..78fd712e40 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
@@ -2,49 +2,84 @@ 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 {
+ AppInstancePersistencePluginOptions,
+ AppDefinition,
+} from "../../common/types/app";
import { usePersistenceAppStatus } from "../../common/hook/use-persistence-app-status";
import PersistencePluginStatus from "../../components/PersistencePluginStatus/PersistencePluginStatus";
+import { GuiAppConfig } from "../../common/supabase-types";
+import { AppCategory } from "../../common/app-category";
-const ethConfig: AppConfig = {
+const ethBrowserAppDefinition: AppDefinition = {
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",
+ category: AppCategory.LedgerBrowser,
+ defaultInstanceName: "My Eth Browser",
+ defaultDescription:
+ "Application for browsing Ethereum ledger blocks, transactions and tokens. Requires Ethereum persistence plugin to work correctly.",
+ defaultPath: "/eth",
+ defaultOptions: {
+ supabaseUrl: "http://localhost:8000",
+ supabaseKey:
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE",
+ supabaseSchema: "ethereum",
+ },
+
+ createAppInstance(app: GuiAppConfig) {
+ const supabaseOptions =
+ app.options as any as AppInstancePersistencePluginOptions;
+
+ if (
+ !supabaseOptions ||
+ !supabaseOptions.supabaseUrl ||
+ !supabaseOptions.supabaseKey ||
+ !supabaseOptions.supabaseSchema
+ ) {
+ throw new Error(
+ `Invalid ethereum app specific options in the database: ${JSON.stringify(supabaseOptions)}`,
+ );
+ }
+
+ return {
+ id: app.id,
+ appName: "Ethereum Browser",
+ instanceName: app.instance_name,
+ description: app.description,
+ path: app.path,
+ options: supabaseOptions,
+ menuEntries: [
+ {
+ title: "Dashboard",
+ url: "/",
+ },
+ {
+ title: "Accounts",
+ url: "/accounts",
+ },
+ ],
+ routes: [
+ {
+ element: ,
+ },
+ {
+ path: "blocks",
+ element: ,
+ },
+ {
+ path: "transactions",
+ element: ,
+ },
+ {
+ path: "accounts",
+ element: ,
+ },
+ ],
+ useAppStatus: () => usePersistenceAppStatus("PluginPersistenceEthereum"),
+ StatusComponent: (
+
+ ),
+ };
},
- menuEntries: [
- {
- title: "Dashboard",
- url: "/",
- },
- {
- title: "Accounts",
- url: "/accounts",
- },
- ],
- routes: [
- {
- element: ,
- },
- {
- path: "blocks",
- element: ,
- },
- {
- path: "transactions",
- element: ,
- },
- {
- path: "accounts",
- element: ,
- },
- ],
- useAppStatus: () => usePersistenceAppStatus("PluginPersistenceEthereum"),
- StatusComponent: (
-
- ),
};
-export default ethConfig;
+export default ethBrowserAppDefinition;
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts
index c64d103b03..aee3f48cd9 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts
@@ -3,7 +3,7 @@
* @todo Move to separate directory if this file becomes too complex.
*/
-import { createClient } from "@supabase/supabase-js";
+import { SupabaseClient, createClient } from "@supabase/supabase-js";
import { queryOptions } from "@tanstack/react-query";
import {
Transaction,
@@ -11,16 +11,10 @@ import {
TokenHistoryItem20,
TokenMetadata721,
TokenERC20,
-} from "../../common/supabase-types";
+} from "./supabase-types";
+import { useEthSupabaseConfig } from "./hooks";
-// TODO - Configure for an app
-const supabaseQueryKey = "supabase:ethereum";
-const supabaseUrl = "__SUPABASE_URL__";
-const supabaseKey = "__SUPABASE_KEY__";
-
-export const supabase = createClient(supabaseUrl, supabaseKey, {
- schema: "ethereum",
-});
+let supabase: SupabaseClient | undefined;
function createQueryKey(
tableName: string,
@@ -29,12 +23,25 @@ function createQueryKey(
return [tableName, { pagination }];
}
+function useSupabaseClient(): [SupabaseClient, string] {
+ const supabaseConfig = useEthSupabaseConfig();
+
+ if (!supabase) {
+ supabase = createClient(supabaseConfig.url, supabaseConfig.key, {
+ schema: supabaseConfig.schema,
+ });
+ }
+
+ return [supabase, `supabase:${supabaseConfig.schema}`];
+}
+
/**
* Get all recorded ethereum transactions.
* Returns `queryOptions` to be used as argument to `useQuery` from `react-query`.
* Supports paging.
*/
export function ethAllTransactionsQuery(page: number, pageSize: number) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const fromIndex = page * pageSize;
const toIndex = fromIndex + pageSize - 1;
const tableName = "transaction";
@@ -70,6 +77,7 @@ export function ethAccountTransactionsQuery(
pageSize: number,
accountAddress: string,
) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const fromIndex = page * pageSize;
const toIndex = fromIndex + pageSize - 1;
const tableName = "transaction";
@@ -101,6 +109,7 @@ export function ethAccountTransactionsQuery(
* Supports paging.
*/
export function ethAllBlocksQuery(page: number, pageSize: number) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const fromIndex = page * pageSize;
const toIndex = fromIndex + pageSize - 1;
const tableName = "block";
@@ -133,6 +142,7 @@ export function ethERC20TokenHistory(
tokenAddress: string,
accountAddress: string,
) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const tableName = "erc20_token_history_view";
return queryOptions({
queryKey: [supabaseQueryKey, tableName, tokenAddress, accountAddress],
@@ -166,6 +176,8 @@ export interface EthAllERC721TokensByAccountResponseType {
* Returns `queryOptions` to be used as argument to `useQuery` from `react-query`.
*/
export function ethAllERC721TokensByAccount(accountAddress: string) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
+
return queryOptions({
queryKey: [supabaseQueryKey, "ethAllERC721TokensByAccount", accountAddress],
queryFn: async () => {
@@ -192,6 +204,8 @@ export function ethAllERC721TokensByAccount(accountAddress: string) {
* Returns `queryOptions` to be used as argument to `useQuery` from `react-query`.
*/
export function ethAllERC20TokensByAccount(accountAddress: string) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
+
return queryOptions({
queryKey: [supabaseQueryKey, "ethAllERC20TokensByAccount", accountAddress],
queryFn: async () => {
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/supabase-types.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/supabase-types.ts
new file mode 100644
index 0000000000..b2172fcd6d
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/supabase-types.ts
@@ -0,0 +1,45 @@
+export interface Transaction {
+ index: number;
+ hash: string;
+ block_number: number;
+ from: string;
+ to: string;
+ eth_value: number;
+ method_signature: string;
+ method_name: string;
+ id: string;
+}
+
+export interface Block {
+ number: number;
+ created_at: string;
+ hash: string;
+ number_of_tx: number;
+ sync_at: string;
+}
+
+export interface TokenHistoryItem20 {
+ transaction_hash: string;
+ token_address: string;
+ created_at: string;
+ sender: string;
+ recipient: string;
+ value: number;
+}
+
+export interface TokenMetadata721 {
+ address: string;
+ name: string;
+ symbol: string;
+ created_at: string;
+}
+
+// Materialized View
+export interface TokenERC20 {
+ account_address: string;
+ balance: number;
+ name: string;
+ symbol: string;
+ total_supply: number;
+ token_address: string;
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDetailsBox.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDetailsBox.tsx
index b7d61dc634..b094946b94 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDetailsBox.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/components/CertificateDetails/CertificateDetailsBox.tsx
@@ -3,7 +3,7 @@ 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 { FabricCertificate } from "../../supabase-types";
import StackedRowItems from "../../../../components/ui/StackedRowItems";
const ListHeaderTypography = styled(Typography)(({ theme }) => ({
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/hooks.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/hooks.tsx
new file mode 100644
index 0000000000..336d2dcc13
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/hooks.tsx
@@ -0,0 +1,16 @@
+import { useOutletContext } from "react-router-dom";
+import { AppInstancePersistencePluginOptions } from "../../common/types/app";
+
+export function useFabricAppConfig() {
+ return useOutletContext();
+}
+
+export function useFabricSupabaseConfig() {
+ const appConfig = useFabricAppConfig();
+
+ return {
+ schema: appConfig.supabaseSchema,
+ url: appConfig.supabaseUrl,
+ key: appConfig.supabaseKey,
+ };
+}
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 437410665c..7f4ede78fa 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/index.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/index.tsx
@@ -1,57 +1,93 @@
+import { Outlet } from "react-router-dom";
+
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 {
+ AppInstancePersistencePluginOptions,
+ AppDefinition,
+} from "../../common/types/app";
import { usePersistenceAppStatus } from "../../common/hook/use-persistence-app-status";
import PersistencePluginStatus from "../../components/PersistencePluginStatus/PersistencePluginStatus";
+import { GuiAppConfig } from "../../common/supabase-types";
+import { AppCategory } from "../../common/app-category";
-const fabricConfig: AppConfig = {
+const fabricBrowserAppDefinition: AppDefinition = {
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",
+ category: AppCategory.LedgerBrowser,
+ defaultInstanceName: "My Fabric Browser",
+ defaultDescription:
+ "Application for browsing Hyperledger Fabric ledger blocks and transactions. Requires Fabric persistence plugin to work correctly.",
+ defaultPath: "/fabric",
+ defaultOptions: {
+ supabaseUrl: "http://localhost:8000",
+ supabaseKey:
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE",
+ supabaseSchema: "fabric",
},
- menuEntries: [
- {
- title: "Dashboard",
- url: "/",
- },
- ],
- routes: [
- {
- element: ,
- },
- {
- path: "blocks",
- element: ,
- },
- {
- path: "transactions",
- element: ,
- },
- {
- path: "transaction",
- element: ,
- children: [
+
+ createAppInstance(app: GuiAppConfig) {
+ const supabaseOptions =
+ app.options as any as AppInstancePersistencePluginOptions;
+
+ if (
+ !supabaseOptions ||
+ !supabaseOptions.supabaseUrl ||
+ !supabaseOptions.supabaseKey ||
+ !supabaseOptions.supabaseSchema
+ ) {
+ throw new Error(
+ `Invalid fabric app specific options in the database: ${JSON.stringify(supabaseOptions)}`,
+ );
+ }
+
+ return {
+ id: app.id,
+ appName: "Hyperledger Fabric Browser",
+ instanceName: app.instance_name,
+ description: app.description,
+ path: app.path,
+ options: supabaseOptions,
+ menuEntries: [
+ {
+ title: "Dashboard",
+ url: "/",
+ },
+ ],
+ routes: [
+ {
+ element: ,
+ },
+ {
+ path: "blocks",
+ element: ,
+ },
+ {
+ path: "transactions",
+ element: ,
+ },
{
- path: ":hash",
- element: (
-
-
-
- ),
+ path: "transaction",
+ element: ,
+ children: [
+ {
+ path: ":hash",
+ element: (
+
+
+
+ ),
+ },
+ ],
},
],
- },
- ],
- useAppStatus: () => usePersistenceAppStatus("PluginPersistenceFabric"),
- StatusComponent: (
-
- ),
+ useAppStatus: () => usePersistenceAppStatus("PluginPersistenceFabric"),
+ StatusComponent: (
+
+ ),
+ };
+ },
};
-export default fabricConfig;
+export default fabricBrowserAppDefinition;
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TranactionInfoCard.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TranactionInfoCard.tsx
index c1be9680dd..d7d0f66fb7 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TranactionInfoCard.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TranactionInfoCard.tsx
@@ -4,7 +4,7 @@ import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Skeleton from "@mui/material/Skeleton";
-import { FabricTransaction } from "../../fabric-supabase-types";
+import { FabricTransaction } from "../../supabase-types";
import ShortenedTypography from "../../../../components/ui/ShortenedTypography";
import StackedRowItems from "../../../../components/ui/StackedRowItems";
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionsTable.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionsTable.tsx
index 947e3ed890..b2a8f81b2b 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionsTable.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/pages/TransactionDetails/TransactionActionsTable.tsx
@@ -19,7 +19,7 @@ import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import { fabricTransactionActions } from "../../queries";
-import { FabricTransactionAction } from "../../fabric-supabase-types";
+import { FabricTransactionAction } from "../../supabase-types";
import {
StyledTableCellHeader,
StyledTableCell,
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/queries.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/queries.ts
index 1bb444189d..5c4e62606b 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/queries.ts
+++ b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/queries.ts
@@ -3,7 +3,7 @@
* @todo Move to separate directory if this file becomes too complex.
*/
-import { createClient } from "@supabase/supabase-js";
+import { SupabaseClient, createClient } from "@supabase/supabase-js";
import { queryOptions } from "@tanstack/react-query";
import {
FabricBlock,
@@ -11,15 +11,10 @@ import {
FabricTransaction,
FabricTransactionAction,
FabricTransactionActionEndorsement,
-} from "./fabric-supabase-types";
+} from "./supabase-types";
+import { useFabricSupabaseConfig } from "./hooks";
-// TODO - Configure for an app
-const supabaseQueryKey = "supabase:fabric";
-const supabaseUrl = "http://localhost:8000";
-const supabaseKey = "__SUPABASE_KEY__";
-export const supabase = createClient(supabaseUrl, supabaseKey, {
- schema: "fabric",
-});
+let supabase: SupabaseClient | undefined;
function createQueryKey(
tableName: string,
@@ -28,12 +23,25 @@ function createQueryKey(
return [tableName, { pagination }];
}
+function useSupabaseClient(): [SupabaseClient, string] {
+ const supabaseConfig = useFabricSupabaseConfig();
+
+ if (!supabase) {
+ supabase = createClient(supabaseConfig.url, supabaseConfig.key, {
+ schema: supabaseConfig.schema,
+ });
+ }
+
+ return [supabase, `supabase:${supabaseConfig.schema}`];
+}
+
/**
* Get all recorded fabric blocks.
* Returns `queryOptions` to be used as argument to `useQuery` from `react-query`.
* Supports paging.
*/
export function fabricAllBlocksQuery(page: number, pageSize: number) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const fromIndex = page * pageSize;
const toIndex = fromIndex + pageSize - 1;
const tableName = "block";
@@ -63,6 +71,7 @@ export function fabricAllBlocksQuery(page: number, pageSize: number) {
* Supports paging.
*/
export function fabricAllTransactionsQuery(page: number, pageSize: number) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const fromIndex = page * pageSize;
const toIndex = fromIndex + pageSize - 1;
const tableName = "transaction";
@@ -91,6 +100,7 @@ export function fabricAllTransactionsQuery(page: number, pageSize: number) {
* Get transaction object form the database using it's hash.
*/
export function fabricTransactionByHash(hash: string) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const tableName = "transaction";
return queryOptions({
@@ -122,6 +132,7 @@ export function fabricTransactionByHash(hash: string) {
* Get transaction actions form the database using parent transaction id.
*/
export function fabricTransactionActions(txId: string) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const tableName = "transaction_action";
return queryOptions({
@@ -147,6 +158,7 @@ export function fabricTransactionActions(txId: string) {
* Get fabric certificate using it's ID.
*/
export function fabricCertificate(id: string) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const tableName = "certificate";
return queryOptions({
@@ -178,6 +190,7 @@ export function fabricCertificate(id: string) {
* Get transaction action endorsements form the database using parent action id.
*/
export function fabricActionEndorsements(actionId: string) {
+ const [supabase, supabaseQueryKey] = useSupabaseClient();
const tableName = "transaction_action_endorsement";
return queryOptions({
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/fabric-supabase-types.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/fabric/supabase-types.ts
similarity index 100%
rename from packages/cacti-ledger-browser/src/main/typescript/apps/fabric/fabric-supabase-types.ts
rename to packages/cacti-ledger-browser/src/main/typescript/apps/fabric/supabase-types.ts
diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/app-category.tsx b/packages/cacti-ledger-browser/src/main/typescript/common/app-category.tsx
new file mode 100644
index 0000000000..7fff430e35
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/common/app-category.tsx
@@ -0,0 +1,34 @@
+import WebIcon from "@mui/icons-material/Web";
+import DnsIcon from "@mui/icons-material/Dns";
+import TokenIcon from "@mui/icons-material/Token";
+
+export enum AppCategory {
+ LedgerBrowser = "ledgerBrowser",
+ Connector = "connector",
+ SampleApp = "sampleApp",
+}
+
+export function getAppCategoryConfig(appConfig: AppCategory) {
+ switch (appConfig) {
+ case AppCategory.LedgerBrowser:
+ return {
+ name: "Ledger Browser",
+ description: "Browse and analyse ledger data persisted in a database",
+ icon: ,
+ };
+ case AppCategory.Connector:
+ return {
+ name: "Connector",
+ description: "Interact with ledgers through Cacti connectors",
+ icon: ,
+ };
+ case AppCategory.SampleApp:
+ return {
+ name: "Sample App",
+ description: "Run sample Cacti application",
+ icon: ,
+ };
+ default:
+ throw new Error(`Unknown App Category provided: ${appConfig}`);
+ }
+}
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 102724e64e..983a8cbcac 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/common/config.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/common/config.tsx
@@ -1,5 +1,10 @@
-import ethereumGuiConfig from "../apps/eth";
-import fabricAppConfig from "../apps/fabric";
-import { AppConfig } from "./types/app";
+import ethBrowserAppDefinition from "../apps/eth";
+import fabricBrowserAppDefinition from "../apps/fabric";
+import { AppDefinition } from "./types/app";
-export const appConfig: AppConfig[] = [ethereumGuiConfig, fabricAppConfig];
+const config = new Map([
+ ["ethereumPersistenceBrowser", ethBrowserAppDefinition],
+ ["fabricPersistenceBrowser", fabricBrowserAppDefinition],
+]);
+
+export default config;
diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/createApplications.tsx b/packages/cacti-ledger-browser/src/main/typescript/common/createApplications.tsx
new file mode 100644
index 0000000000..a41292391c
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/common/createApplications.tsx
@@ -0,0 +1,29 @@
+import config from "./config";
+import { GuiAppConfig } from "./supabase-types";
+import { AppInstance } from "./types/app";
+
+export default function createApplications(appsFromDb?: GuiAppConfig[]) {
+ const appConfig = [] as AppInstance[];
+
+ if (!appsFromDb) {
+ return appConfig;
+ }
+
+ for (const app of appsFromDb) {
+ try {
+ const appDefinition = config.get(app.app_id);
+
+ if (!appDefinition) {
+ throw new Error(
+ `Unknown app ID found in the database - ${app.app_id}, ensure you're using latest GUI version!`,
+ );
+ }
+
+ appConfig.push(appDefinition.createAppInstance(app));
+ } catch (error) {
+ console.error(`Could not add app ${app.app_id}: ${error}`);
+ }
+ }
+
+ return appConfig;
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/queries.ts b/packages/cacti-ledger-browser/src/main/typescript/common/queries.ts
index 786bc979bd..5b7fc81f56 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/common/queries.ts
+++ b/packages/cacti-ledger-browser/src/main/typescript/common/queries.ts
@@ -1,17 +1,32 @@
-import { createClient } from "@supabase/supabase-js";
-import { queryOptions } from "@tanstack/react-query";
-import { PluginStatus } from "./supabase-types";
+import { SupabaseClient, createClient } from "@supabase/supabase-js";
+import { QueryClient, queryOptions } from "@tanstack/react-query";
+import { GuiAppConfig, PluginStatus } from "./supabase-types";
+import { AddGuiAppConfigType, UpdateGuiAppConfigType } from "./types/app";
-const supabaseQueryKey = "supabase";
-const supabaseUrl = "__SUPABASE_URL__";
-const supabaseKey = "__SUPABASE_KEY__";
+let supabase: SupabaseClient | undefined;
-export const supabase = createClient(supabaseUrl, supabaseKey);
+/**
+ * Get or initialize (if not already done) a supabase client using environment variables.
+ */
+function getSupabaseClient(): [SupabaseClient, string] {
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseKey = import.meta.env.VITE_SUPABASE_KEY;
+ const supabaseSchema = import.meta.env.VITE_SUPABASE_SCHEMA;
+
+ if (!supabase) {
+ supabase = createClient(supabaseUrl, supabaseKey, {
+ schema: supabaseSchema,
+ });
+ }
+
+ return [supabase, `supabase:${supabaseSchema}`];
+}
/**
* Get persistence plugin status from the database using it's name.
*/
export function persistencePluginStatus(name: string) {
+ const [supabase, supabaseQueryKey] = getSupabaseClient();
const tableName = "plugin_status";
return queryOptions({
@@ -38,3 +53,124 @@ export function persistencePluginStatus(name: string) {
},
});
}
+
+/**
+ * Get persistence plugin app config from the database.
+ */
+export function guiAppConfig() {
+ const [supabase, supabaseQueryKey] = getSupabaseClient();
+ const tableName = "gui_app_config";
+
+ return queryOptions({
+ queryKey: [supabaseQueryKey, tableName],
+ queryFn: async () => {
+ const { data, error } = await supabase.from(tableName).select();
+
+ if (error) {
+ throw new Error(
+ `Could not get GUI App configuration: ${error.message}`,
+ );
+ }
+
+ return data as GuiAppConfig[];
+ },
+ });
+}
+
+/**
+ * Get single persistence plugin app instance infofrom the database.
+ */
+export function guiAppConfigById(id: string) {
+ const [supabase, supabaseQueryKey] = getSupabaseClient();
+ const tableName = "gui_app_config";
+
+ return queryOptions({
+ queryKey: [supabaseQueryKey, tableName, id],
+ queryFn: async () => {
+ const { data, error } = await supabase
+ .from(tableName)
+ .select()
+ .eq("id", id);
+
+ if (error) {
+ throw new Error(
+ `Could not get app instance (id ${id}) configuration: ${error.message}`,
+ );
+ }
+
+ if (data.length !== 1) {
+ throw new Error(
+ `Invalid response when getting app instance with id ${id}: ${data}`,
+ );
+ }
+
+ return data.pop() as GuiAppConfig;
+ },
+ });
+}
+
+/**
+ * Invalidate all queries from gui_app_config.
+ * Call after each mutation that affects this table.
+ */
+export function invalidateGuiAppConfig(queryClient: QueryClient) {
+ const [, supabaseQueryKey] = getSupabaseClient();
+ queryClient.invalidateQueries({
+ queryKey: [supabaseQueryKey, "gui_app_config"],
+ });
+}
+
+/**
+ * Add new GUI app configuration to the database.
+ */
+export async function addGuiAppConfig(appData: AddGuiAppConfigType) {
+ const [supabase] = getSupabaseClient();
+ const { data, error } = await supabase
+ .from("gui_app_config")
+ .insert([appData]);
+
+ if (error) {
+ throw new Error(`Could not insert GUI App configuration: ${error.message}`);
+ }
+
+ return data;
+}
+
+/**
+ * Update GUI app configuration in the database.
+ */
+export async function updateGuiAppConfig(
+ id: string,
+ appData: UpdateGuiAppConfigType,
+) {
+ const [supabase] = getSupabaseClient();
+ const { data, error } = await supabase
+ .from("gui_app_config")
+ .update([appData])
+ .eq("id", id);
+
+ if (error) {
+ throw new Error(
+ `Could not update GUI App ${id} configuration: ${error.message}`,
+ );
+ }
+
+ return data;
+}
+
+/**
+ * Delete GUI app configuration from the database.
+ */
+export async function deleteGuiAppConfig(id: string) {
+ const [supabase] = getSupabaseClient();
+ const { data, error } = await supabase
+ .from("gui_app_config")
+ .delete()
+ .eq("id", id);
+
+ if (error) {
+ throw new Error(`Could not delete GUI App ${id}, error: ${error.message}`);
+ }
+
+ return data;
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/supabase-client.tsx b/packages/cacti-ledger-browser/src/main/typescript/common/supabase-client.tsx
deleted file mode 100644
index f8f8863a66..0000000000
--- a/packages/cacti-ledger-browser/src/main/typescript/common/supabase-client.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { createClient } from "@supabase/supabase-js";
-import { queryOptions } from "@tanstack/react-query";
-
-export const supabaseQueryKey = "supabase";
-const supabaseUrl = "__SUPABASE_URL__";
-const supabaseKey = "__SUPABASE_KEY__";
-
-export const supabase = createClient(supabaseUrl, supabaseKey);
-
-/**
- * React Query config to fetch entire table from supabase.
- */
-export function supabaseQueryTable(tableName: string) {
- return queryOptions({
- queryKey: [supabaseQueryKey, tableName],
- queryFn: async () => {
- const { data, error } = await supabase.from(tableName).select();
- if (error) {
- throw new Error(
- `Could not get data from '${tableName}' table: ${error.message}`,
- );
- }
-
- return data as T;
- },
- });
-}
-
-async function getMatchingTableEntries(
- tableName: string,
- query: Record,
-) {
- const { data, error } = await supabase.from(tableName).select().match(query);
- if (error) {
- throw new Error(
- `Could not get data from '${tableName}' table using query '${query}': ${error.message}`,
- );
- }
-
- return data;
-}
-
-export function supabaseQueryAllMatchingEntries(
- tableName: string,
- query: Record,
-) {
- return queryOptions({
- queryKey: [supabaseQueryKey, tableName, query],
- queryFn: () => {
- return getMatchingTableEntries(tableName, query) as T;
- },
- });
-}
-
-export function supabaseQuerySingleMatchingEntry(
- tableName: string,
- query: Record,
-) {
- return queryOptions({
- queryKey: [supabaseQueryKey, tableName, query],
- queryFn: async () => {
- const data = await getMatchingTableEntries(tableName, query);
- if (data.length > 1) {
- console.warn(`${tableName} query ${query} returned more than 1 entry!`);
- }
- return data[0] as T;
- },
- });
-}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/supabase-types.ts b/packages/cacti-ledger-browser/src/main/typescript/common/supabase-types.ts
index 465a7701f4..972480ffe6 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/common/supabase-types.ts
+++ b/packages/cacti-ledger-browser/src/main/typescript/common/supabase-types.ts
@@ -1,114 +1,3 @@
-export interface ERC20Txn {
- account_address: string;
- token_address: string;
- uri: string;
- token_id: number;
- id: string;
- balance: number;
- last_owner_change: string;
-}
-
-export interface ERC721Txn {
- account_address: string;
- token_address: string;
- uri: string;
- token_id: number;
- id: string;
- last_owner_change: string;
-}
-
-export interface TokenMetadata20 {
- address: string;
- name: string;
- symbol: string;
- total_supply: number;
- created_at: string;
-}
-
-export interface TokenMetadata721 {
- address: string;
- name: string;
- symbol: string;
- created_at: string;
-}
-
-export interface Block {
- number: number;
- created_at: string;
- hash: string;
- number_of_tx: number;
- sync_at: string;
-}
-
-export interface TokenTransfer {
- transaction_id: string;
- sender: string;
- recipient: string;
- value: number;
- id: string;
-}
-
-export interface Transaction {
- index: number;
- hash: string;
- block_number: number;
- from: string;
- to: string;
- eth_value: number;
- method_signature: string;
- method_name: string;
- id: string;
-}
-
-export interface TokenHistoryItem {
- transaction_hash: string;
- token_address: string;
- created_at: string;
- sender: string;
- recipient: string;
-}
-
-export interface TokenHistoryItem721 extends TokenHistoryItem {
- token_id: number;
-}
-
-export interface TokenHistoryItem20 extends TokenHistoryItem {
- value: number;
-}
-
-export interface TokenTransactionMetadata721 {
- account_address: string;
- token_address: string;
- uri: string;
- symbol: string;
-}
-
-export interface TableProperty {
- display: string;
- objProp: string[];
-}
-
-export interface TableRowClick {
- action: (param: string) => void;
- prop: string;
-}
-export interface TableProps {
- onClick: TableRowClick;
- schema: TableProperty[];
-}
-
-/// MANUAL EDITS
-
-// Materialized View
-export interface TokenERC20 {
- account_address: string;
- balance: number;
- name: string;
- symbol: string;
- total_supply: number;
- token_address: string;
-}
-
export interface PluginStatus {
name: string;
last_instance_id: string;
@@ -116,3 +5,14 @@ export interface PluginStatus {
created_at: string;
last_connected_at: string;
}
+
+export interface GuiAppConfig {
+ id: string;
+ app_id: string;
+ instance_name: string;
+ description: string;
+ path: string;
+ options: Record;
+ created_at: string;
+ updated_at: string;
+}
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 3374b557aa..7da191ab78 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
@@ -1,12 +1,13 @@
import React from "react";
import { RouteObject } from "react-router-dom";
+import { GuiAppConfig } from "../supabase-types";
export interface AppListEntry {
path: string;
name: string;
}
-export interface AppConfigMenuEntry {
+export interface AppInstanceMenuEntry {
title: string;
url: string;
}
@@ -22,17 +23,42 @@ export interface GetStatusResponse {
status: AppStatus;
}
-export interface AppConfigOptions {
- instanceName: string;
- description: string | undefined;
- path: string;
+export interface AppInstancePersistencePluginOptions {
+ supabaseSchema: string;
+ supabaseUrl: string;
+ supabaseKey: string;
}
-export interface AppConfig {
+export interface AppInstance {
+ id: string;
appName: string;
- options: AppConfigOptions;
- menuEntries: AppConfigMenuEntry[];
+ instanceName: string;
+ description: string | undefined;
+ path: string;
+ options: T;
+ menuEntries: AppInstanceMenuEntry[];
routes: RouteObject[];
useAppStatus: () => GetStatusResponse;
StatusComponent: React.ReactElement;
}
+
+export type CreateAppInstanceFactoryType = (app: GuiAppConfig) => AppInstance;
+
+export interface AppDefinition {
+ appName: string;
+ category: string;
+ defaultInstanceName: string;
+ defaultDescription: string;
+ defaultPath: string;
+ defaultOptions: unknown;
+ createAppInstance: CreateAppInstanceFactoryType;
+}
+
+export type UpdateGuiAppConfigType = {
+ instance_name: string;
+ description: string;
+ path: string;
+ options: unknown;
+};
+
+export type AddGuiAppConfigType = UpdateGuiAppConfigType & { app_id: string };
diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/AppSetupForms/AppOptionsForm.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/AppSetupForms/AppOptionsForm.tsx
new file mode 100644
index 0000000000..ef266b946e
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/components/AppSetupForms/AppOptionsForm.tsx
@@ -0,0 +1,46 @@
+import * as React from "react";
+import Box from "@mui/material/Box";
+import TextField from "@mui/material/TextField";
+
+export interface AppOptionsFormProps {
+ validationError: string;
+ setValidationError: React.Dispatch>;
+ appOptionsJsonString: string;
+ setAppOptionsJsonString: React.Dispatch>;
+}
+
+/**
+ * Form component for editing app options (app specific settings)
+ */
+export default function AppOptionsForm({
+ validationError,
+ setValidationError,
+ appOptionsJsonString,
+ setAppOptionsJsonString,
+}: AppOptionsFormProps) {
+ return (
+
+ {
+ setValidationError("");
+ setAppOptionsJsonString(e.target.value);
+ }}
+ />
+
+ );
+}
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
new file mode 100644
index 0000000000..a93788bed5
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/components/AppSetupForms/AppSetupForm.tsx
@@ -0,0 +1,92 @@
+import * as React from "react";
+import Box from "@mui/material/Box";
+import TextField from "@mui/material/TextField";
+
+const emptyFormHelperText = "Field can't be empty";
+const regularPathHelperText =
+ "Path under which the plugin will be available, must be unique withing GUI.";
+const illformedPathHelperText = "Must be valid path (starting with '/')";
+
+export interface CommonSetupFormValues {
+ instanceName: string;
+ description: string;
+ path: string;
+}
+
+export interface AppSetupFormProps {
+ commonSetupValues: CommonSetupFormValues;
+ setCommonSetupValues: React.Dispatch<
+ React.SetStateAction
+ >;
+}
+
+/**
+ * Form component for editing common app options.
+ */
+export default function AppSetupForm({
+ commonSetupValues,
+ setCommonSetupValues,
+}: AppSetupFormProps) {
+ const isInstanceNameEmptyError = !!!commonSetupValues.instanceName;
+ const isDescriptionEmptyError = !!!commonSetupValues.description;
+ const isPathEmptyError = !!!commonSetupValues.path;
+ const isPathInvalidError = !(
+ commonSetupValues.path.startsWith("/") && commonSetupValues.path.length > 1
+ );
+ let pathHelperText = regularPathHelperText;
+ if (isPathEmptyError) {
+ pathHelperText = emptyFormHelperText;
+ } else if (isPathInvalidError) {
+ pathHelperText = illformedPathHelperText;
+ }
+
+ const handleChange = (
+ e: React.ChangeEvent,
+ ) => {
+ setCommonSetupValues({
+ ...commonSetupValues,
+ [e.target.name]: e.target.value,
+ });
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/ConnectionFailedDialog/ConnectionFailedDialog.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/ConnectionFailedDialog/ConnectionFailedDialog.tsx
new file mode 100644
index 0000000000..d1b692c541
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/components/ConnectionFailedDialog/ConnectionFailedDialog.tsx
@@ -0,0 +1,79 @@
+import * as React from "react";
+import Dialog from "@mui/material/Dialog";
+import Typography from "@mui/material/Typography";
+import Slide from "@mui/material/Slide";
+import Box from "@mui/material/Box";
+import Paper from "@mui/material/Paper";
+import { TransitionProps } from "@mui/material/transitions";
+
+const Transition = React.forwardRef(function Transition(
+ props: TransitionProps & {
+ children: React.ReactElement;
+ },
+ ref: React.Ref,
+) {
+ return ;
+});
+
+/**
+ * Error dialog that covers entire screen in case of connection error.
+ * Can't be closed or dismissed.
+ *
+ * @todo extend the guidliness, link to the documentation once it's ready.
+ */
+export default function ConnectionFailedDialog() {
+ // const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ // const supabaseKey = import.meta.env.VITE_SUPABASE_KEY;
+ // const supabaseSchema = import.meta.env.VITE_SUPABASE_SCHEMA;
+
+ return (
+
+ );
+}
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 7f23912c14..14b1b6c4d9 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
@@ -7,13 +7,13 @@ import IconButton from "@mui/material/IconButton";
import AppsIcon from "@mui/icons-material/Apps";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
-import { AppConfigMenuEntry, AppListEntry } from "../../common/types/app";
+import { AppInstanceMenuEntry, AppListEntry } from "../../common/types/app";
import { patchAppRoutePath } from "../../common/utils";
type HeaderBarProps = {
appList: AppListEntry[];
path?: string;
- menuEntries?: AppConfigMenuEntry[];
+ menuEntries?: AppInstanceMenuEntry[];
};
const HeaderBar: React.FC = ({ path, menuEntries }) => {
diff --git a/packages/cacti-ledger-browser/src/main/typescript/main.tsx b/packages/cacti-ledger-browser/src/main/typescript/main.tsx
index 610c719003..06a555e567 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/main.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/main.tsx
@@ -3,11 +3,10 @@ import "@mui/material/styles/styled";
import * as React from "react";
import * as ReactDOM from "react-dom/client";
-import { appConfig } from "./common/config";
import CactiLedgerBrowserApp from "./CactiLedgerBrowserApp";
ReactDOM.createRoot(document.getElementById("root")!).render(
-
+
,
);
diff --git a/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/AddNewApp.tsx b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/AddNewApp.tsx
new file mode 100644
index 0000000000..7e96de45d9
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/AddNewApp.tsx
@@ -0,0 +1,169 @@
+import * as React from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import Box from "@mui/material/Box";
+import Stepper from "@mui/material/Stepper";
+import Step from "@mui/material/Step";
+import StepLabel from "@mui/material/StepLabel";
+
+import config from "../../common/config";
+import { AppCategory } from "../../common/app-category";
+import { addGuiAppConfig, invalidateGuiAppConfig } from "../../common/queries";
+import { useNotification } from "../../common/context/NotificationContext";
+import AppSpecificSetupView from "./AppSpecificSetupView";
+import CommonSetupView from "./CommonSetupView";
+import SelectAppView from "./SelectAppView";
+import SelectGroupView from "./SelectGroupView";
+
+const steps = [
+ "Select Group",
+ "Select App",
+ "Common Setup",
+ "App Specific Setup",
+];
+
+export interface AddNewAppProps {
+ handleDone: () => void;
+}
+
+/**
+ * Complex view with steps used to select and setup new GUI application.
+ */
+export default function AddNewApp({ handleDone }: AddNewAppProps) {
+ const queryClient = useQueryClient();
+ const addGuiAppMutation = useMutation({
+ mutationFn: addGuiAppConfig,
+ onSuccess: () => invalidateGuiAppConfig(queryClient),
+ });
+ const { showNotification } = useNotification();
+ const [activeStep, setActiveStep] = React.useState(0);
+ const [appCategory, setAppCategory] = React.useState("");
+ const [appId, setAppId] = React.useState("");
+ const [commonSetupValues, setCommonSetupValues] = React.useState({
+ instanceName: "",
+ description: "",
+ path: "",
+ });
+ const [appOptionsJsonString, setAppOptionsJsonString] = React.useState("");
+
+ // Handle app creation error
+ React.useEffect(() => {
+ if (addGuiAppMutation.isError) {
+ showNotification(
+ `Could not fetch action endorsements: ${addGuiAppMutation.error}`,
+ "error",
+ );
+ addGuiAppMutation.reset();
+ }
+ }, [addGuiAppMutation.isError]);
+
+ // Handle app creation success
+ React.useEffect(() => {
+ if (addGuiAppMutation.isSuccess) {
+ showNotification(
+ `Application ${commonSetupValues.instanceName} added successfully`,
+ "success",
+ );
+ addGuiAppMutation.reset();
+ handleDone();
+ }
+ }, [addGuiAppMutation.isSuccess]);
+
+ const handleNextStep = () => {
+ setActiveStep((prevActiveStep) => prevActiveStep + 1);
+ };
+
+ const handleBackStep = () => {
+ setActiveStep((prevActiveStep) => prevActiveStep - 1);
+ };
+
+ // Select current view in a steper
+ let currentPage: JSX.Element | undefined;
+ switch (activeStep) {
+ case 0:
+ currentPage = (
+ {
+ setAppCategory(category);
+ handleNextStep();
+ }}
+ />
+ );
+ break;
+ case 1:
+ currentPage = (
+ {
+ setAppId(appId);
+
+ // Fetch setup defaults to be used in later views
+ const appDefinition = config.get(appId);
+ if (!appDefinition) {
+ throw new Error(`Could not find App Definition with id ${appId}`);
+ }
+ setCommonSetupValues({
+ instanceName: appDefinition.defaultInstanceName,
+ description: appDefinition.defaultDescription,
+ path: appDefinition.defaultPath,
+ });
+ setAppOptionsJsonString(
+ JSON.stringify(appDefinition.defaultOptions, undefined, 2),
+ );
+
+ handleNextStep();
+ }}
+ handleBack={() => {
+ setAppId("");
+ setAppCategory("");
+ handleBackStep();
+ }}
+ />
+ );
+ break;
+ case 2:
+ currentPage = (
+
+ );
+ break;
+ case 3:
+ currentPage = (
+ {
+ addGuiAppMutation.mutate({
+ app_id: appId,
+ instance_name: commonSetupValues.instanceName,
+ description: commonSetupValues.description,
+ path: commonSetupValues.path,
+ options: JSON.parse(appOptionsJsonString),
+ });
+ }}
+ />
+ );
+ break;
+ }
+
+ // Render the stepper view
+ return (
+
+
+ {steps.map((label) => {
+ return (
+
+ {label}
+
+ );
+ })}
+
+ {currentPage}
+
+ );
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/AppSpecificSetupView.tsx b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/AppSpecificSetupView.tsx
new file mode 100644
index 0000000000..f75df74ed3
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/AppSpecificSetupView.tsx
@@ -0,0 +1,72 @@
+import * as React from "react";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Typography from "@mui/material/Typography";
+import LoadingButton from "@mui/lab/LoadingButton";
+import SaveIcon from "@mui/icons-material/Save";
+import AppOptionsForm from "../../components/AppSetupForms/AppOptionsForm";
+
+export interface AppSpecificSetupViewProps {
+ appOptionsJsonString: string;
+ setAppOptionsJsonString: React.Dispatch>;
+ handleBack: () => void;
+ handleSave: () => void;
+ isSending: boolean;
+}
+
+/**
+ * Add new app stepper view containing application specific configuration (in form of a JSON to be filled by the user)
+ */
+export default function AppSpecificSetupView({
+ appOptionsJsonString,
+ setAppOptionsJsonString,
+ handleBack,
+ handleSave,
+ isSending,
+}: AppSpecificSetupViewProps) {
+ const [validationError, setValidationError] = React.useState("");
+
+ return (
+ <>
+ App Specific Setup
+
+
+
+ }
+ variant="contained"
+ onClick={() => {
+ // Validate JSON input
+ try {
+ JSON.parse(appOptionsJsonString);
+ } catch (error) {
+ setValidationError(`Invalid JSON format, error: ${error}`);
+ return;
+ }
+
+ handleSave();
+ }}
+ >
+ Save
+
+
+ >
+ );
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/CommonSetupView.tsx b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/CommonSetupView.tsx
new file mode 100644
index 0000000000..f1ee05b741
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/CommonSetupView.tsx
@@ -0,0 +1,55 @@
+import * as React from "react";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Typography from "@mui/material/Typography";
+import AppSetupForm from "../../components/AppSetupForms/AppSetupForm";
+
+export interface CommonSetupFormValues {
+ instanceName: string;
+ description: string;
+ path: string;
+}
+
+export interface CommonSetupViewProps {
+ commonSetupValues: CommonSetupFormValues;
+ setCommonSetupValues: React.Dispatch<
+ React.SetStateAction
+ >;
+ handleBack: () => void;
+ handleNext: () => void;
+}
+
+/**
+ * Add new app stepper view containing common application configuration (required by all apps).
+ */
+export default function CommonSetupView({
+ commonSetupValues,
+ setCommonSetupValues,
+ handleBack,
+ handleNext,
+}: CommonSetupViewProps) {
+ return (
+ <>
+ Common App Setup
+
+
+
+
+
+ >
+ );
+}
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
new file mode 100644
index 0000000000..42663bed25
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/SelectAppView.tsx
@@ -0,0 +1,59 @@
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Typography from "@mui/material/Typography";
+import List from "@mui/material/List";
+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 config from "../../common/config";
+import { AppCategory, getAppCategoryConfig } from "../../common/app-category";
+
+export interface SelectAppViewProps {
+ appCategory: string;
+ handleAppSelected: (appId: string) => void;
+ handleBack: () => void;
+}
+
+/**
+ * Add new app stepper view containing list of app under given category to pick.
+ */
+export default function SelectAppView({
+ appCategory,
+ handleAppSelected,
+ handleBack,
+}: SelectAppViewProps) {
+ const apps = Array.from(config).filter(
+ (app) => app[1].category === appCategory,
+ );
+ const categoryConfig = getAppCategoryConfig(appCategory as AppCategory);
+
+ return (
+ <>
+ Select Application
+
+
+ {apps.map(([appId, app]) => {
+ return (
+ handleAppSelected(appId)}>
+
+ {categoryConfig.icon}
+
+
+
+ );
+ })}
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/SelectGroupView.tsx b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/SelectGroupView.tsx
new file mode 100644
index 0000000000..5c0cab42a6
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/pages/add-new-app/SelectGroupView.tsx
@@ -0,0 +1,52 @@
+import Typography from "@mui/material/Typography";
+import List from "@mui/material/List";
+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 config from "../../common/config";
+import { AppCategory, getAppCategoryConfig } from "../../common/app-category";
+
+export interface SelectGroupViewProps {
+ handleCategorySelected: (category: AppCategory) => void;
+}
+
+/**
+ * Add new app stepper view containing list of app categories to select.
+ */
+export default function SelectGroupView({
+ handleCategorySelected,
+}: SelectGroupViewProps) {
+ const appCategories = Array.from(config.values()).map((app) => app.category);
+
+ return (
+ <>
+ Select Group
+
+
+ {Object.values(AppCategory).map((category) => {
+ const categoryConfig = getAppCategoryConfig(category);
+ const appCount = appCategories.filter(
+ (appCat) => appCat === category,
+ ).length;
+ const categoryTitle = `${categoryConfig.name} (${appCount})`;
+
+ return (
+ handleCategorySelected(category)}
+ >
+
+ {categoryConfig.icon}
+
+
+
+ );
+ })}
+
+ >
+ );
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/pages/configure-app/ConfigureApp.tsx b/packages/cacti-ledger-browser/src/main/typescript/pages/configure-app/ConfigureApp.tsx
new file mode 100644
index 0000000000..bb1bd64741
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/pages/configure-app/ConfigureApp.tsx
@@ -0,0 +1,267 @@
+import * as React from "react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import Box from "@mui/material/Box";
+import Dialog from "@mui/material/Dialog";
+import DialogTitle from "@mui/material/DialogTitle";
+import DialogContent from "@mui/material/DialogContent";
+import Typography from "@mui/material/Typography";
+import CircularProgress from "@mui/material/CircularProgress";
+import Button from "@mui/material/Button";
+import DialogContentText from "@mui/material/DialogContentText";
+import DialogActions from "@mui/material/DialogActions";
+import LoadingButton from "@mui/lab/LoadingButton";
+import SaveIcon from "@mui/icons-material/Save";
+import DeleteIcon from "@mui/icons-material/Delete";
+
+import {
+ deleteGuiAppConfig,
+ guiAppConfigById,
+ invalidateGuiAppConfig,
+ updateGuiAppConfig,
+} from "../../common/queries";
+import { UpdateGuiAppConfigType } from "../../common/types/app";
+import { useNotification } from "../../common/context/NotificationContext";
+import AppSetupForm from "../../components/AppSetupForms/AppSetupForm";
+import AppOptionsForm from "../../components/AppSetupForms/AppOptionsForm";
+
+type DeleteWithConfirmationButtonProps = {
+ appInstanceId: string;
+ handleDone: () => void;
+};
+
+/**
+ * Button and logic for removing the application from a database.
+ */
+function DeleteWithConfirmationButton({
+ appInstanceId,
+ handleDone,
+}: DeleteWithConfirmationButtonProps) {
+ const { showNotification } = useNotification();
+ const queryClient = useQueryClient();
+ const [openDialog, setOpenDialog] = React.useState(false);
+ const deleteGuiAppMutation = useMutation({
+ mutationFn: () => deleteGuiAppConfig(appInstanceId),
+ onSuccess: () => invalidateGuiAppConfig(queryClient),
+ });
+
+ const handleClose = () => {
+ setOpenDialog(false);
+ };
+
+ // Show error if app can't be deleted from the database
+ React.useEffect(() => {
+ if (deleteGuiAppMutation.isError) {
+ showNotification(
+ `Could not delete application ${appInstanceId}, error: ${deleteGuiAppMutation.error}`,
+ "error",
+ );
+ deleteGuiAppMutation.reset();
+ }
+ }, [deleteGuiAppMutation.isError]);
+
+ // Show success message and terminate if data was saved successfully
+ React.useEffect(() => {
+ if (deleteGuiAppMutation.isSuccess) {
+ showNotification(
+ `Application ${appInstanceId} removed successfully`,
+ "success",
+ );
+ deleteGuiAppMutation.reset();
+ handleDone();
+ }
+ }, [deleteGuiAppMutation.isSuccess]);
+
+ return (
+ <>
+ }
+ onClick={() => setOpenDialog(true)}
+ sx={{ marginRight: 1 }}
+ variant="contained"
+ >
+ Delete
+
+
+
+ >
+ );
+}
+
+export interface ConfigureAppProps {
+ appInstanceId: string;
+ handleDone: () => void;
+}
+
+/**
+ * View to edit or delete an application.
+ */
+export default function ConfigureApp({
+ appInstanceId,
+ handleDone,
+}: ConfigureAppProps) {
+ const queryClient = useQueryClient();
+ const { showNotification } = useNotification();
+ const [jsonValidationError, setJsonValidationError] = React.useState("");
+ const [commonSetupValues, setCommonSetupValues] = React.useState({
+ instanceName: "",
+ description: "",
+ path: "",
+ });
+ const [appOptionsJsonString, setAppOptionsJsonString] = React.useState("");
+ const updateGuiAppMutation = useMutation({
+ mutationFn: (data: UpdateGuiAppConfigType) =>
+ updateGuiAppConfig(appInstanceId, data),
+ onSuccess: () => invalidateGuiAppConfig(queryClient),
+ });
+ const appConfigQuery = useQuery(guiAppConfigById(appInstanceId));
+ const appConfigData = appConfigQuery.data;
+
+ // Set current app configuration values to the form once data is received from the database
+ React.useEffect(() => {
+ if (appConfigData && !appConfigQuery.isPending) {
+ setCommonSetupValues({
+ instanceName: appConfigData.instance_name,
+ description: appConfigData.description,
+ path: appConfigData.path,
+ });
+ setAppOptionsJsonString(
+ JSON.stringify(appConfigData.options, undefined, 2),
+ );
+ }
+ }, [appConfigData]);
+
+ // Show error if current data can't be fetched from the database
+ React.useEffect(() => {
+ if (appConfigQuery.isError) {
+ showNotification(
+ `Could not fetch ${appInstanceId} application config: ${appConfigQuery.error}`,
+ "error",
+ );
+ }
+ }, [appConfigQuery.isError]);
+
+ // Show error if updates can't be saved to the database
+ React.useEffect(() => {
+ if (updateGuiAppMutation.isError) {
+ showNotification(
+ `Could not save ${appInstanceId} updated application config: ${updateGuiAppMutation.error}`,
+ "error",
+ );
+ updateGuiAppMutation.reset();
+ }
+ }, [updateGuiAppMutation.isError]);
+
+ // Show success message and terminate if data was saved successfully
+ React.useEffect(() => {
+ if (updateGuiAppMutation.isSuccess) {
+ showNotification(
+ `Application ${commonSetupValues.instanceName} edited successfully`,
+ "success",
+ );
+ updateGuiAppMutation.reset();
+ handleDone();
+ }
+ }, [updateGuiAppMutation.isSuccess]);
+
+ // Render the view
+ return (
+
+ {appConfigQuery.isPending && (
+
+ )}
+
+ Common App Setup
+
+
+
+ App Specific Setup
+
+
+
+
+
+
+
+
+ }
+ variant="contained"
+ onClick={() => {
+ // Validate JSON input
+ try {
+ JSON.parse(appOptionsJsonString);
+ } catch (error) {
+ setJsonValidationError(`Invalid JSON format, error: ${error}`);
+ return;
+ }
+
+ updateGuiAppMutation.mutate({
+ instance_name: commonSetupValues.instanceName,
+ description: commonSetupValues.description,
+ path: commonSetupValues.path,
+ options: JSON.parse(appOptionsJsonString),
+ });
+ }}
+ >
+ Save
+
+
+
+
+ );
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/pages/home/AddApplicationPopupCard.tsx b/packages/cacti-ledger-browser/src/main/typescript/pages/home/AddApplicationPopupCard.tsx
new file mode 100644
index 0000000000..b01b45bbc4
--- /dev/null
+++ b/packages/cacti-ledger-browser/src/main/typescript/pages/home/AddApplicationPopupCard.tsx
@@ -0,0 +1,95 @@
+import React from "react";
+import { useTheme } from "@mui/material/styles";
+import Card from "@mui/material/Card";
+import CardActionArea from "@mui/material/CardActionArea";
+import Typography from "@mui/material/Typography";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Dialog from "@mui/material/Dialog";
+import DialogContent from "@mui/material/DialogContent";
+import DialogTitle from "@mui/material/DialogTitle";
+import AddIcon from "@mui/icons-material/Add";
+import CloseIcon from "@mui/icons-material/Close";
+import AddNewApp from "../add-new-app/AddNewApp";
+
+export interface NewAppDialogProps {
+ open: boolean;
+ setOpen: React.Dispatch>;
+}
+
+function NewAppDialog({ open, setOpen }: NewAppDialogProps) {
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ return (
+ <>
+
+ >
+ );
+}
+
+export default function AddApplicationPopupCard() {
+ const theme = useTheme();
+ const [open, setOpen] = React.useState(false);
+
+ return (
+ <>
+
+ {
+ setOpen(true);
+ }}
+ sx={{
+ flex: 1,
+ }}
+ >
+
+
+ Add Application
+
+
+
+
+ >
+ );
+}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/pages/home/AppCard.tsx b/packages/cacti-ledger-browser/src/main/typescript/pages/home/AppCard.tsx
index 5386824c74..3517da884f 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/pages/home/AppCard.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/pages/home/AppCard.tsx
@@ -12,7 +12,8 @@ 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";
+import { AppInstance, AppStatus } from "../../common/types/app";
+import ConfigureApp from "../configure-app/ConfigureApp";
type StatusTextProps = {
status: AppStatus;
@@ -73,8 +74,36 @@ function StatusDialogButton({ statusComponent }: StatusDialogButtonProps) {
);
}
+type ConfigureDialogButtonProps = {
+ appInstanceId: string;
+};
+
+function ConfigureDialogButton({ appInstanceId }: ConfigureDialogButtonProps) {
+ const [openDialog, setOpenDialog] = React.useState(false);
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
type AppCardProps = {
- appConfig: AppConfig;
+ appConfig: AppInstance;
};
/**
@@ -98,7 +127,7 @@ export default function AppCard({ appConfig }: AppCardProps) {
>
{
- navigate(appConfig.options.path);
+ navigate(appConfig.path);
}}
>
- {appConfig.options.instanceName}
+ {appConfig.instanceName}
{appConfig.appName}
- {appConfig.options.description && (
-
- {appConfig.options.description}
-
+ {appConfig.description && (
+ {appConfig.description}
)}
Initialized:{" "}
@@ -144,6 +171,7 @@ export default function AppCard({ appConfig }: AppCardProps) {
borderColor: theme.palette.primary.main,
}}
>
+
diff --git a/packages/cacti-ledger-browser/src/main/typescript/pages/home/HomePage.tsx b/packages/cacti-ledger-browser/src/main/typescript/pages/home/HomePage.tsx
index 2539bcbb67..bfac7cfddd 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/pages/home/HomePage.tsx
+++ b/packages/cacti-ledger-browser/src/main/typescript/pages/home/HomePage.tsx
@@ -1,10 +1,15 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
-import { appConfig } from "../../common/config";
+import { AppInstance } from "../../common/types/app";
import AppCard from "./AppCard";
+import AddApplicationPopupCard from "./AddApplicationPopupCard";
-export default function HomePage() {
+type HomePageProps = {
+ appConfig: AppInstance[];
+};
+
+export default function HomePage({ appConfig }: HomePageProps) {
return (
@@ -13,17 +18,13 @@ export default function HomePage() {
+
{appConfig.map((a) => {
- return (
-
- );
+ return ;
})}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/vite-env.d.ts b/packages/cacti-ledger-browser/src/main/typescript/vite-env.d.ts
index 11f02fe2a0..8b854e00df 100644
--- a/packages/cacti-ledger-browser/src/main/typescript/vite-env.d.ts
+++ b/packages/cacti-ledger-browser/src/main/typescript/vite-env.d.ts
@@ -1 +1,11 @@
///
+
+interface ImportMetaEnv {
+ readonly VITE_SUPABASE_URL: string
+ readonly VITE_SUPABASE_KEY: string
+ readonly VITE_SUPABASE_SCHEMA: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
\ No newline at end of file
diff --git a/packages/cacti-ledger-browser/tsconfig.json b/packages/cacti-ledger-browser/tsconfig.json
index c46a23bf1a..6fdc580da1 100644
--- a/packages/cacti-ledger-browser/tsconfig.json
+++ b/packages/cacti-ledger-browser/tsconfig.json
@@ -1,6 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
"composite": true,
"outDir": "./dist/lib/",
"declarationDir": "dist/lib",