diff --git a/.vscode/settings.json b/.vscode/settings.json index 842de7a2..1b82453d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,10 @@ "**/.next": true, "**/out": true }, - "typescript.preferences.importModuleSpecifier": "non-relative" + "typescript.preferences.importModuleSpecifier": "non-relative", + "files.watcherExclude": { + "**/node_modules/**": true, + "**/dist/**": true, + "**/.git/**": true + } } diff --git a/package.json b/package.json index 47c29d27..a2832484 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "@libp2p/peer-id": "^4.1.4", "@libp2p/ping": "^1.1.1", "@libp2p/websockets": "^8.1.1", - "@mui/icons-material": "^7.3.4", - "@mui/material": "^7.3.4", + "@mui/icons-material": "^7.3.6", + "@mui/material": "^7.3.6", "@mui/x-data-grid": "^8.14.1", "@multiformats/multiaddr": "^12", "@oceanprotocol/contracts": "2.5.0", @@ -40,6 +40,7 @@ "formik": "^2.4.9", "gsap": "3.13.0", "it-pipe": "^3.0.1", + "json-edit-react": "^1.29.0", "libp2p": "^1.8.0", "myetherwallet-blockies": "^0.1.1", "next": "^15.4.8", @@ -65,7 +66,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^6.6.0", - "prettier": "^3.3.3", + "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.3.0", "typescript": "^5" }, diff --git a/src/assets/logo.svg b/src/assets/logo.svg index 9d54579f..ec658224 100644 --- a/src/assets/logo.svg +++ b/src/assets/logo.svg @@ -1,9 +1,9 @@ - + - - + + @@ -22,8 +22,8 @@ - - + + diff --git a/src/components/node-details/config-modal.tsx b/src/components/node-details/config-modal.tsx new file mode 100644 index 00000000..f4540790 --- /dev/null +++ b/src/components/node-details/config-modal.tsx @@ -0,0 +1,56 @@ +import Button from '@/components/button/button'; +import Modal from '@/components/modal/modal'; +import { JsonEditor } from 'json-edit-react'; +import { Dispatch, SetStateAction } from 'react'; +import styles from './node-info.module.css'; + +type ConfigModalProps = { + isOpen: boolean; + fetchingConfig: boolean; + pushingConfig: boolean; + config: Record; + editedConfig: Record; + setEditedConfig: Dispatch>>; + handlePushConfig: (config: Record) => Promise; + onClose: () => void; +}; + +const ConfigModal = ({ + isOpen, + fetchingConfig, + pushingConfig, + config, + editedConfig, + setEditedConfig, + handlePushConfig, + onClose, +}: ConfigModalProps) => { + return ( + +
+ {fetchingConfig && (!config || Object.keys(config).length === 0) ? ( +
Fetching config...
+ ) : ( +
+ setEditedConfig(newData as Record)} + collapse={({ value }) => typeof value === 'object' && value !== null && Object.keys(value).length === 0} + /> + +
+ )} +
+ + ); +}; + +export default ConfigModal; diff --git a/src/components/node-details/environments.tsx b/src/components/node-details/environments.tsx index 357096bc..0cf5f28e 100644 --- a/src/components/node-details/environments.tsx +++ b/src/components/node-details/environments.tsx @@ -5,7 +5,7 @@ import { EnvNodeInfo } from '@/types/environments'; import styles from './environments.module.css'; type EnvironmentsProps = { - nodeInfo: EnvNodeInfo; + nodeInfo: EnvNodeInfo; }; const Environments = ({ nodeInfo }: EnvironmentsProps) => { @@ -15,8 +15,10 @@ const Environments = ({ nodeInfo }: EnvironmentsProps) => {

Environments

- {!isReady ?
Fetching data...
: envs.map((env) => - + {!isReady ? ( +
Fetching data...
+ ) : ( + envs.map((env) => ) )}
diff --git a/src/components/node-details/node-info.module.css b/src/components/node-details/node-info.module.css index 674c7324..e290d901 100644 --- a/src/components/node-details/node-info.module.css +++ b/src/components/node-details/node-info.module.css @@ -34,6 +34,28 @@ display: flex; gap: 8px; } + + .modalContent { + display: flex; + flex-direction: column; + gap: 24px; + + .modalActions { + display: flex; + justify-content: flex-end; + padding-top: 16px; + border-top: 1px solid var(--border-glass); + } + } + + .fetching { + display: flex; + justify-content: center; + align-items: center; + padding: 40px; + color: var(--text-secondary); + font-style: italic; + } } .infoFooter { diff --git a/src/components/node-details/node-info.tsx b/src/components/node-details/node-info.tsx index 90305df2..14d08bea 100644 --- a/src/components/node-details/node-info.tsx +++ b/src/components/node-details/node-info.tsx @@ -2,12 +2,17 @@ import Button from '@/components/button/button'; import Card from '@/components/card/card'; import { Balance } from '@/components/node-details/balance'; import Eligibility from '@/components/node-details/eligibility'; +import { useP2P } from '@/contexts/P2PContext'; +import { useOceanAccount } from '@/lib/use-ocean-account'; import { Node, NodeEligibility } from '@/types/nodes'; +import { useAuthModal, useSignMessage, useSmartAccountClient } from '@account-kit/react'; import DnsIcon from '@mui/icons-material/Dns'; -import DownloadIcon from '@mui/icons-material/Download'; import LocationPinIcon from '@mui/icons-material/LocationPin'; import PublicIcon from '@mui/icons-material/Public'; import UploadIcon from '@mui/icons-material/Upload'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import ConfigModal from './config-modal'; import styles from './node-info.module.css'; type NodeInfoProps = { @@ -15,6 +20,91 @@ type NodeInfoProps = { }; const NodeInfo = ({ node }: NodeInfoProps) => { + const { client } = useSmartAccountClient({ type: 'LightAccount' }); + const { signMessageAsync } = useSignMessage({ + client, + }); + const { openAuthModal } = useAuthModal(); + const { account, ocean } = useOceanAccount(); + const { config, fetchConfig, pushConfig } = useP2P(); + + const [fetchingConfig, setFetchingConfig] = useState(false); + const [pushingConfig, setPushingConfig] = useState(false); + const [isEditConfigDialogOpen, setIsEditConfigDialogOpen] = useState(false); + const [editedConfig, setEditedConfig] = useState>({}); + + useEffect(() => { + if (config) { + setEditedConfig(config); + } + }, [config]); + + async function handleFetchConfig() { + if (!account.isConnected) { + openAuthModal(); + return; + } + if (!ocean || !node?.id) { + return; + } + const timestamp = Date.now() + 5 * 60 * 1000; // 5 minutes expiry + const signedMessage = await signMessageAsync({ + message: timestamp.toString(), + }); + + setFetchingConfig(true); + try { + await fetchConfig(node.id, signedMessage, timestamp, account.address as string); + } catch (error) { + console.error('Error fetching node config :', error); + } finally { + setFetchingConfig(false); + } + } + + async function handlePushConfig(config: Record) { + let success = false; + if (!account.isConnected) { + openAuthModal(); + return; + } + if (!ocean || !node?.id) { + return; + } + const timestamp = Date.now() + 5 * 60 * 1000; // 5 minutes expiry + const signedMessage = await signMessageAsync({ + message: timestamp.toString(), + }); + + setPushingConfig(true); + try { + await pushConfig(node.id, signedMessage, timestamp, config, account.address as string); + success = true; + } catch (error) { + console.error('Error pushing node config :', error); + } finally { + setPushingConfig(false); + if (success) { + toast.success('Successfully pushed new config!'); + setIsEditConfigDialogOpen(false); + } else { + toast.error('Failed to push new config'); + } + } + } + + function handleOpenEditConfigModal() { + if (!config || Object.keys(config).length === 0) { + handleFetchConfig(); + } + + setIsEditConfigDialogOpen(true); + } + + function handleCloseModal() { + setIsEditConfigDialogOpen(false); + } + return (
@@ -39,10 +129,19 @@ const NodeInfo = ({ node }: NodeInfoProps) => {
- -
diff --git a/src/components/node-details/unban-requests.tsx b/src/components/node-details/unban-requests.tsx index b5f3977a..aa177a8f 100644 --- a/src/components/node-details/unban-requests.tsx +++ b/src/components/node-details/unban-requests.tsx @@ -44,11 +44,12 @@ const UnbanRequests = ({ node }: UnbanRequestsProps) => { } setLoading(true); try { - const timestamp = Date.now(); + const timestamp = Date.now() + 5 * 60 * 1000; // 5 minutes expiry const signedMessage = await signMessageAsync({ message: timestamp.toString(), }); - await requestNodeUnban(node.id, signedMessage, timestamp); + + await requestNodeUnban(node.id, signedMessage as string, timestamp, account.address as string); await fetchUnbanRequests(node.id); } catch (error) { console.error('Error requesting unban:', error); diff --git a/src/context/unban-requests-context.tsx b/src/context/unban-requests-context.tsx index 8970e016..653483dc 100644 --- a/src/context/unban-requests-context.tsx +++ b/src/context/unban-requests-context.tsx @@ -7,7 +7,7 @@ type UnbanRequestsContextType = { unbanRequests: UnbanRequest[]; fetchUnbanRequests: (nodeId: string) => Promise; - requestNodeUnban: (nodeId: string, signature: string, expiryTimestamp: number) => Promise; + requestNodeUnban: (nodeId: string, signature: string, expiryTimestamp: number, address: string) => Promise; }; const UnbanRequestsContext = createContext(undefined); @@ -29,16 +29,20 @@ export const UnbanRequestsProvider = ({ children }: { children: ReactNode }) => } }, []); - const requestNodeUnban = useCallback(async (nodeId: string, signature: string, expiryTimestamp: number) => { - try { - await axios.post(`${getApiRoute('nodeUnbanRequests')}/${nodeId}/unban`, { - signature, - expiryTimestamp, - }); - } catch (error) { - console.error('Error requesting node unban: ', error); - } - }, []); + const requestNodeUnban = useCallback( + async (nodeId: string, signature: string, expiryTimestamp: number, address: string) => { + try { + await axios.post(`${getApiRoute('nodeUnbanRequests')}/${nodeId}/unban`, { + signature, + expiryTimestamp, + address, + }); + } catch (error) { + console.error('Error requesting node unban: ', error); + } + }, + [] + ); return ( ; node: Libp2p | null; isReady: boolean; error: string | null; envs: ComputeEnvironment[]; - sendCommand: (peerId: string, command: any, protocol?: string) => Promise; + fetchConfig: (peerId: string, signature: string, expiryTimestamp: number, address: string) => Promise; getEnvs: (peerId: string) => Promise; + pushConfig: ( + peerId: string, + signature: string, + expiryTimestamp: number, + config: Record, + address: string + ) => Promise; + sendCommand: (peerId: string, command: any, protocol?: string) => Promise; } const P2PContext = createContext(undefined); export function P2PProvider({ children }: { children: React.ReactNode }) { + const [config, setConfig] = useState>({}); const [envs, setEnvs] = useState([]); const [node, setNode] = useState(null); const [error, setError] = useState(null); @@ -78,14 +95,47 @@ export function P2PProvider({ children }: { children: React.ReactNode }) { [isReady, node] ); + const fetchConfig = useCallback( + async (peerId: string, signature: string, expiryTimestamp: number, address: string) => { + if (!isReady || !node) { + throw new Error('Node not ready'); + } + const result = await fetchNodeConfig(peerId, signature, expiryTimestamp, address); + + setConfig(result); + }, + [isReady, node] + ); + + const pushConfig = useCallback( + async ( + peerId: string, + signature: string, + expiryTimestamp: number, + config: Record, + address: string + ) => { + if (!isReady || !node) { + throw new Error('Node not ready'); + } + await pushNodeConfig(peerId, signature, expiryTimestamp, config, address); + + setConfig(config); + }, + [isReady, node] + ); + return ( diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 9cf3ef3e..49e0b561 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -14,6 +14,7 @@ import type { AppProps } from 'next/app'; import { Inter, Orbitron } from 'next/font/google'; import { useEffect, useRef } from 'react'; import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; const inter = Inter({ subsets: ['latin'], diff --git a/src/services/nodeService.ts b/src/services/nodeService.ts index 2615df1a..4951bd5a 100644 --- a/src/services/nodeService.ts +++ b/src/services/nodeService.ts @@ -339,6 +339,20 @@ export async function getNodeEnvs(peerId: string) { return sendCommandToPeer(peerId, { command: 'getComputeEnvironments', node: peerId }); } +export async function fetchNodeConfig(peerId: string, signature: string, expiryTimestamp: number, address: string) { + return sendCommandToPeer(peerId, { command: 'fetchConfig', signature, expiryTimestamp, address }); +} + +export async function pushNodeConfig( + peerId: string, + signature: string, + expiryTimestamp: number, + config: Record, + address: string +) { + return sendCommandToPeer(peerId, { command: 'pushConfig', signature, expiryTimestamp, config, address }); +} + export async function stopNode() { if (nodeInstance) { await nodeInstance.stop(); diff --git a/src/styles/globals.css b/src/styles/globals.css index b62346b2..fda120b2 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -39,19 +39,21 @@ margin: 0; } -html { - background-color: var(--background-black); -} html, body { - max-width: 100vw; - overflow-x: hidden; font-family: var(--font-inter), sans-serif; font-size: 14px; line-height: 1.5; font-style: normal; color: var(--text-primary); } +html { + background-color: var(--background-black); +} +body { + max-width: 100vw; + overflow-x: hidden; +} h1, h2, diff --git a/yarn.lock b/yarn.lock index db36ed63..c06e2552 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3426,7 +3426,7 @@ __metadata: languageName: node linkType: hard -"@mui/icons-material@npm:^7.3.4": +"@mui/icons-material@npm:^7.3.6": version: 7.3.6 resolution: "@mui/icons-material@npm:7.3.6" dependencies: @@ -3442,7 +3442,7 @@ __metadata: languageName: node linkType: hard -"@mui/material@npm:^7.3.4": +"@mui/material@npm:^7.3.6": version: 7.3.6 resolution: "@mui/material@npm:7.3.6" dependencies: @@ -13963,6 +13963,18 @@ __metadata: languageName: node linkType: hard +"json-edit-react@npm:^1.29.0": + version: 1.29.0 + resolution: "json-edit-react@npm:1.29.0" + dependencies: + object-property-assigner: "npm:^1.3.5" + object-property-extractor: "npm:^1.0.13" + peerDependencies: + react: ">=16.0.0" + checksum: 10c0/c2f0ce4435cc9bf78504592b9cbfab36e6d41020c47632ea757bf999e4924cc67dee429ddfebcd3a1212a7386feeddab6145b0f5285138e2cdbbe5e46301403c + languageName: node + linkType: hard + "json-parse-even-better-errors@npm:^2.3.0": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -15174,6 +15186,20 @@ __metadata: languageName: node linkType: hard +"object-property-assigner@npm:^1.3.5": + version: 1.3.5 + resolution: "object-property-assigner@npm:1.3.5" + checksum: 10c0/144ecbc656c1ea25aac074a41ee32b1aa2935dcbb126750a0f7dc4d454af835e19c43931ed230b966f624b846cf92e4e524719d87eecc9deadb7602b1168906b + languageName: node + linkType: hard + +"object-property-extractor@npm:^1.0.13": + version: 1.0.13 + resolution: "object-property-extractor@npm:1.0.13" + checksum: 10c0/53c1de766f313dbdb0f169b04b641add3397a32dec71e780794bbc6dadd0235e6e194e9e829b78c497a3229079e66f8fc1737ec4e1da85fa1a79f0400991965b + languageName: node + linkType: hard + "object.assign@npm:^4.1.4, object.assign@npm:^4.1.7": version: 4.1.7 resolution: "object.assign@npm:4.1.7" @@ -15258,8 +15284,8 @@ __metadata: "@libp2p/peer-id": "npm:^4.1.4" "@libp2p/ping": "npm:^1.1.1" "@libp2p/websockets": "npm:^8.1.1" - "@mui/icons-material": "npm:^7.3.4" - "@mui/material": "npm:^7.3.4" + "@mui/icons-material": "npm:^7.3.6" + "@mui/material": "npm:^7.3.6" "@mui/x-data-grid": "npm:^8.14.1" "@multiformats/multiaddr": "npm:^12" "@oceanprotocol/contracts": "npm:2.5.0" @@ -15285,11 +15311,12 @@ __metadata: formik: "npm:^2.4.9" gsap: "npm:3.13.0" it-pipe: "npm:^3.0.1" + json-edit-react: "npm:^1.29.0" libp2p: "npm:^1.8.0" myetherwallet-blockies: "npm:^0.1.1" next: "npm:^15.4.8" normalize.css: "npm:^8.0.1" - prettier: "npm:^3.3.3" + prettier: "npm:^3.7.4" prettier-plugin-organize-imports: "npm:^4.3.0" react: "npm:^18" react-dom: "npm:^18" @@ -15853,7 +15880,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.3.3": +"prettier@npm:^3.7.4": version: 3.7.4 resolution: "prettier@npm:3.7.4" bin: