Skip to content

Commit e3a6e2d

Browse files
authored
Node storage feature (#457)
* add node storage feature * fix warning * handle already loaded * handle missing account address * handle SCA accounts * refactor create modal * navigate to bucket files by clicking bucket row * format errors * persist auth token * create bucket without access list * hide edit access button for buckets without access list * add access list in bucket files page * update labels
1 parent cbc289f commit e3a6e2d

42 files changed

Lines changed: 2671 additions & 173 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@mui/material": "^7.3.6",
2626
"@mui/x-data-grid": "^8.14.1",
2727
"@oceanprotocol/contracts": "2.6.0",
28-
"@oceanprotocol/lib": "8.0.3",
28+
"@oceanprotocol/lib": "8.0.6",
2929
"@ramp-network/ramp-instant-sdk": "^6.2.0",
3030
"@tanstack/react-query": "^5.28.4",
3131
"@wagmi/core": "^2.15.0",

src/components/Navigation/profile-button.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import Avatar from '@/components/avatar/avatar';
22
import Menu from '@/components/menu/menu';
3+
import EditAccessListModal from '@/components/node-storage/edit-access-list-modal';
34
import { useProfileContext } from '@/context/profile-context';
45
import { useOceanAccount } from '@/lib/use-ocean-account';
56
import { GrantStatus } from '@/types/grant';
67
import { formatWalletAddress } from '@/utils/formatters';
78
import { useAuthModal, useLogout } from '@account-kit/react';
9+
import ListAltIcon from '@mui/icons-material/ListAlt';
810
import LogoutIcon from '@mui/icons-material/Logout';
911
import PersonIcon from '@mui/icons-material/Person';
1012
import RedeemIcon from '@mui/icons-material/Redeem';
@@ -16,18 +18,20 @@ import { useEffect, useMemo, useRef, useState } from 'react';
1618
import Button from '../button/button';
1719
import styles from './navigation.module.css';
1820

19-
const ProfileButton = () => {
21+
const ProfileButton: React.FC = () => {
2022
const router = useRouter();
2123

2224
const { closeAuthModal, isOpen: isAuthModalOpen, openAuthModal } = useAuthModal();
2325
const { isLoggingOut, logout } = useLogout();
2426

25-
const { account } = useOceanAccount();
27+
const { account, provider } = useOceanAccount();
2628

2729
const { ensName, ensProfile, grantStatus } = useProfileContext();
2830

2931
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
3032
const [isClient, setIsClient] = useState(false);
33+
const [isAccessListModalOpen, setIsAccessListModalOpen] = useState(false);
34+
3135
const buttonRef = useRef<HTMLButtonElement>(null);
3236

3337
// This is a workaround for the modal not closing after connecting
@@ -136,6 +140,20 @@ const ProfileButton = () => {
136140
</ListItemIcon>
137141
Convert to COMPY
138142
</MenuItem>
143+
{provider && (
144+
<MenuItem
145+
disableRipple
146+
onClick={() => {
147+
setIsAccessListModalOpen(true);
148+
handleCloseMenu();
149+
}}
150+
>
151+
<ListItemIcon>
152+
<ListAltIcon />
153+
</ListItemIcon>
154+
Edit access list
155+
</MenuItem>
156+
)}
139157
<MenuItem
140158
sx={{
141159
color: 'var(--error-darker)',
@@ -152,6 +170,11 @@ const ProfileButton = () => {
152170
Log out
153171
</MenuItem>
154172
</Menu>
173+
<EditAccessListModal
174+
currentAccount={account.address}
175+
isOpen={isAccessListModalOpen}
176+
onClose={() => setIsAccessListModalOpen(false)}
177+
/>
155178
</>
156179
) : (
157180
<Button className={styles.loginButton} color="accent1" loading={isLoggingOut} onClick={openAuthModal}>

src/components/button/copy-button.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ import { useState } from 'react';
55

66
type CopyButtonProps = Pick<ButtonProps, 'className' | 'color' | 'size' | 'variant'> & {
77
contentToCopy: string;
8+
label?: string;
9+
labelCopied?: string;
810
};
911

10-
const CopyButton = ({
12+
const CopyButton: React.FC<CopyButtonProps> = ({
1113
className,
1214
color = 'accent2',
1315
contentToCopy,
16+
label = 'Copy',
17+
labelCopied = 'Copied!',
1418
size = 'sm',
1519
variant = 'filled',
16-
}: CopyButtonProps) => {
20+
}) => {
1721
const [copied, setCopied] = useState(false);
1822

1923
const handleClick = () => {
@@ -33,7 +37,7 @@ const CopyButton = ({
3337
size={size}
3438
variant={variant}
3539
>
36-
{copied ? 'Copied!' : 'Copy'}
40+
{copied ? labelCopied : label}
3741
</Button>
3842
);
3943
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Button from '@/components/button/button';
2+
import Modal from '@/components/modal/modal';
3+
4+
type ConfirmModalProps = {
5+
confirmLabel?: string;
6+
isOpen: boolean;
7+
message: string;
8+
onCancel: () => void;
9+
onConfirm: () => void;
10+
title?: string;
11+
};
12+
13+
const ConfirmModal: React.FC<ConfirmModalProps> = ({
14+
confirmLabel = 'Confirm',
15+
isOpen,
16+
message,
17+
onCancel,
18+
onConfirm,
19+
title = 'Confirm',
20+
}) => {
21+
return (
22+
<Modal isOpen={isOpen} onClose={onCancel} title={title} width="xs" fullWidth>
23+
<p style={{ margin: 0 }}>{message}</p>
24+
<div className="actionsGroupMdEnd">
25+
<Button color="accent1" onClick={onCancel} size="md" variant="outlined" type="button">
26+
Cancel
27+
</Button>
28+
<Button color="accent1" onClick={onConfirm} size="md" variant="filled" type="button">
29+
{confirmLabel}
30+
</Button>
31+
</div>
32+
</Modal>
33+
);
34+
};
35+
36+
export default ConfirmModal;

src/components/modal/modal.module.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
11
/* Header */
22
.header {
33
align-items: start;
4+
border-bottom: 1px solid var(--border-glass);
5+
box-shadow: var(--drop-shadow-black);
46
display: flex;
57
gap: 24px;
68
justify-content: space-between;
9+
padding: 16px;
10+
position: sticky;
11+
top: 0;
12+
z-index: 1;
13+
14+
@media (min-width: 576px) {
15+
padding: 16px 24px;
16+
}
17+
}
18+
19+
/* Body */
20+
.body {
21+
display: flex;
22+
flex-direction: column;
23+
gap: 16px;
24+
overflow-y: auto;
25+
padding: 16px;
26+
27+
@media (min-width: 576px) {
28+
gap: 24px;
29+
padding: 24px;
30+
}
731
}
832

933
/* Title */

src/components/modal/modal.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@ const StyledDialog = styled(Dialog)(({ theme }) => ({
1616
color: 'var(--text-primary)',
1717
display: 'flex',
1818
flexDirection: 'column',
19-
gap: 24,
20-
padding: 24,
19+
overflow: 'hidden',
20+
padding: 0,
2121

2222
[theme.breakpoints.down('sm')]: {
2323
borderRadius: 16,
24-
gap: 16,
2524
margin: 16,
26-
padding: 16,
2725
width: 'calc(100% - 32px)',
2826
},
2927
},
@@ -49,7 +47,7 @@ const Modal = ({ children, fullWidth, hideCloseButton, isOpen, onClose, title, w
4947
</button>
5048
)}
5149
</div>
52-
{children}
50+
<div className={styles.body}>{children}</div>
5351
</StyledDialog>
5452
);
5553

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import Container from '@/components/container/container';
2+
import SectionTitle from '@/components/section-title/section-title';
3+
import TabBar from '@/components/tab-bar/tab-bar';
4+
import { CircularProgress } from '@mui/material';
5+
import React from 'react';
6+
7+
type TabKey = 'info' | 'storage';
8+
9+
type NodePageLayoutProps = {
10+
activeTab: TabKey;
11+
children?: React.ReactNode;
12+
isWalletConnected?: boolean;
13+
loading?: boolean;
14+
nodeId?: string;
15+
notFound?: boolean;
16+
subtitle: string;
17+
};
18+
19+
const NodeDetailsPageLayout: React.FC<NodePageLayoutProps> = ({
20+
activeTab,
21+
children,
22+
isWalletConnected,
23+
loading,
24+
nodeId,
25+
notFound,
26+
subtitle,
27+
}) => {
28+
if (loading) {
29+
return (
30+
<Container className="pageRoot">
31+
<SectionTitle
32+
moreReadable
33+
title="Node details"
34+
subTitle={
35+
<div className="flexRow alignItemsCenter gapMd">
36+
<CircularProgress size={24} />
37+
<span>Retrieving node details...</span>
38+
</div>
39+
}
40+
/>
41+
</Container>
42+
);
43+
}
44+
45+
if (notFound) {
46+
return (
47+
<Container className="pageRoot">
48+
<SectionTitle moreReadable title="Node details" subTitle="Node not found" />
49+
</Container>
50+
);
51+
}
52+
53+
const tabs: { key: TabKey; label: string; href: string }[] = [
54+
{ key: 'info', label: 'Node info', href: `/nodes/${nodeId}` },
55+
{ key: 'storage' as TabKey, label: 'Remote storage', href: `/nodes/${nodeId}/storage` },
56+
];
57+
58+
return (
59+
<Container className="pageRoot">
60+
<SectionTitle
61+
contentBetween={isWalletConnected ? <TabBar activeKey={activeTab} tabs={tabs} /> : null}
62+
moreReadable
63+
subTitle={subtitle}
64+
title="Node details"
65+
/>
66+
<div className="pageContentWrapper">{children}</div>
67+
</Container>
68+
);
69+
};
70+
71+
export default NodeDetailsPageLayout;

src/components/node-details/node-details-page.tsx

Lines changed: 29 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1-
import Container from '@/components/container/container';
21
import BenchmarkJobs from '@/components/node-details/benchmark-jobs';
32
import Environments from '@/components/node-details/environments';
43
import JobsRevenueStats from '@/components/node-details/jobs-revenue-stats';
4+
import NodeDetailsPageLayout from '@/components/node-details/node-details-page-layout';
55
import NodeInfo from '@/components/node-details/node-info';
66
import UnbanRequests from '@/components/node-details/unban-requests';
7-
import SectionTitle from '@/components/section-title/section-title';
87
import { useNodesContext } from '@/context/nodes-context';
98
import { useUnbanRequestsContext } from '@/context/unban-requests-context';
109
import { useP2P } from '@/contexts/P2PContext';
1110
import { directNodeCommand } from '@/lib/direct-node-command';
11+
import { useOceanAccount } from '@/lib/use-ocean-account';
1212
import { ComputeEnvironment } from '@/types/environments';
13-
import { CircularProgress } from '@mui/material';
1413
import { useParams } from 'next/navigation';
1514
import { useEffect, useMemo, useState } from 'react';
1615

17-
const NodeDetailsPage = () => {
16+
const NodeDetailsPage: React.FC = () => {
1817
const params = useParams<{ nodeId: string }>();
1918

19+
const { account } = useOceanAccount();
20+
2021
const { getEnvs: getEnvsP2P, isReady: isP2PReady, sendCommand } = useP2P();
2122

2223
const { selectedNode, fetchNode, loadingFetchNode } = useNodesContext();
@@ -79,52 +80,31 @@ const NodeDetailsPage = () => {
7980
}
8081
}, [fetchUnbanRequests, node]);
8182

82-
if (loadingFetchNode) {
83-
return (
84-
<Container className="pageRoot">
85-
<SectionTitle
86-
moreReadable
87-
title="Node details"
88-
subTitle={
89-
<div className="flexRow alignItemsCenter gapMd">
90-
<CircularProgress size={24} />
91-
<span>Retrieving node details...</span>
92-
</div>
93-
}
94-
/>
95-
</Container>
96-
);
97-
}
98-
99-
if (!node) {
100-
return (
101-
<Container className="pageRoot">
102-
<SectionTitle moreReadable title="Node details" subTitle="Node not found" />
103-
</Container>
104-
);
105-
}
106-
10783
return (
108-
<Container className="pageRoot">
109-
<SectionTitle
110-
moreReadable
111-
title="Node details"
112-
subTitle="Check node status, performance, and available resources before running a job"
113-
/>
114-
<div className="pageContentWrapper">
115-
<NodeInfo envs={nodeEnvs} node={node} nodeOnline={connectedP2P || connectedDirectNodeCommand} />
116-
<JobsRevenueStats envs={nodeEnvs} />
117-
<BenchmarkJobs />
118-
<Environments
119-
envs={nodeEnvs}
120-
nodeInfo={{
121-
friendlyName: node.friendlyName,
122-
id: node.id ?? node.nodeId,
123-
}}
124-
/>
125-
{node.banned === false && unbanRequests?.length === 0 ? null : <UnbanRequests node={node} />}
126-
</div>
127-
</Container>
84+
<NodeDetailsPageLayout
85+
activeTab="info"
86+
isWalletConnected={account.isConnected}
87+
loading={loadingFetchNode}
88+
nodeId={node?.id ?? node?.nodeId}
89+
notFound={!node}
90+
subtitle="Check node status, performance, and available resources before running a job"
91+
>
92+
{node ? (
93+
<>
94+
<NodeInfo envs={nodeEnvs} node={node} nodeOnline={connectedP2P || connectedDirectNodeCommand} />
95+
<JobsRevenueStats envs={nodeEnvs} />
96+
<BenchmarkJobs />
97+
<Environments
98+
envs={nodeEnvs}
99+
nodeInfo={{
100+
friendlyName: node.friendlyName,
101+
id: node.id ?? node.nodeId,
102+
}}
103+
/>
104+
{node.banned === false && unbanRequests?.length === 0 ? null : <UnbanRequests node={node} />}
105+
</>
106+
) : null}
107+
</NodeDetailsPageLayout>
128108
);
129109
};
130110

0 commit comments

Comments
 (0)