diff --git a/.gitignore b/.gitignore index b8800f5aa6..3942ca6e40 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ cactus-openapi-spec-*.json build/ .gradle/ site/ +.env .build-cache/*.tsbuildinfo diff --git a/packages/cacti-ledger-browser/.env.template b/packages/cacti-ledger-browser/.env.template new file mode 100644 index 0000000000..03f2a4e0f1 --- /dev/null +++ b/packages/cacti-ledger-browser/.env.template @@ -0,0 +1,3 @@ +VITE_SUPABASE_URL=__SUPABSE_URL__ +VITE_SUPABASE_KEY=__SUPABASE_KEY__ +VITE_SUPABASE_SCHEMA=public \ No newline at end of file diff --git a/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx b/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx index 12e43e24dd..2be409ec8e 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx @@ -1,28 +1,37 @@ -import { useRoutes, BrowserRouter, RouteObject } from "react-router-dom"; +import { + useRoutes, + BrowserRouter, + RouteObject, + Outlet, +} from "react-router-dom"; import CssBaseline from "@mui/material/CssBaseline"; +import CircularProgress from "@mui/material/CircularProgress"; import { ThemeProvider, createTheme } from "@mui/material/styles"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { + QueryClient, + QueryClientProvider, + useQuery, +} from "@tanstack/react-query"; // import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { themeOptions } from "./theme"; import ContentLayout from "./components/Layout/ContentLayout"; import HeaderBar from "./components/Layout/HeaderBar"; import HomePage from "./pages/home/HomePage"; -import { AppConfig, AppListEntry } from "./common/types/app"; +import { AppInstance, AppListEntry } from "./common/types/app"; import { patchAppRoutePath } from "./common/utils"; import { NotificationProvider } from "./common/context/NotificationContext"; - -type AppConfigProps = { - appConfig: AppConfig[]; -}; +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: AppConfig[]) { +function getAppList(appConfig: AppInstance[]) { const appList: AppListEntry[] = appConfig.map((app) => { return { - path: app.options.path, + path: app.path, name: app.appName, }; }); @@ -38,17 +47,17 @@ function getAppList(appConfig: AppConfig[]) { /** * Create header bar for each app based on app menuEntries field in config. */ -function getHeaderBarRoutes(appConfig: AppConfig[]) { +function getHeaderBarRoutes(appConfig: AppInstance[]) { const appList = getAppList(appConfig); const headerRoutesConfig = appConfig.map((app) => { return { - key: app.options.path, - path: `${app.options.path}/*`, + key: app.path, + path: `${app.path}/*`, element: ( ), @@ -65,15 +74,16 @@ function getHeaderBarRoutes(appConfig: AppConfig[]) { /** * Create content routes */ -function getContentRoutes(appConfig: AppConfig[]) { +function getContentRoutes(appConfig: AppInstance[]) { const appRoutes: RouteObject[] = appConfig.map((app) => { return { - key: app.options.path, - path: app.options.path, + key: app.path, + path: app.path, + element: , children: app.routes.map((route) => { return { key: route.path, - path: patchAppRoutePath(app.options.path, route.path), + path: patchAppRoutePath(app.path, route.path), element: route.element, children: route.children, }; @@ -84,7 +94,7 @@ function getContentRoutes(appConfig: AppConfig[]) { // Include landing / welcome page appRoutes.push({ index: true, - element: , + element: , }); return useRoutes([ @@ -96,17 +106,35 @@ function getContentRoutes(appConfig: AppConfig[]) { ]); } -const App: React.FC = ({ 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 ( + + + + + Connection Error + + + + We were unable to connect to the Supabase instance containing the + app configuration data for this GUI. + + + Please follow the setup guide to resolve the issue. + + + {/* + Connection Details + +
    +
  • + VITE_SUPABASE_URL:{" "} + {supabaseUrl} +
  • +
  • + VITE_SUPABASE_KEY:{" "} + {supabaseKey} +
  • +
  • + VITE_SUPABASE_SCHEMA:{" "} + {supabaseSchema} +
  • +
+ + + If the connection details are invalid, please update them in the + .env file, then rebuild and run the application again. + */} +
+
+
+ ); +} 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 + + + + Confirm Deletion + + + Are you sure you want to delete this application? This action is + irreversible. You will need to set it up again if you proceed. + + + + + + + + + ); +} + +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 ( + <> + + + Add Application + + + + + + + + ); +} + +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 ( + <> + + setOpenDialog(false)} + open={openDialog} + > + Configure Application + + setOpenDialog(false)} + /> + + + + ); +} + 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",