Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"gsap": "3.13.0",
"it-pipe": "^3.0.1",
"json-edit-react": "^1.29.0",
"jszip": "^3.10.1",
"libp2p": "^1.8.0",
"myetherwallet-blockies": "^0.1.1",
"next": "^15.4.8",
Expand Down
84 changes: 84 additions & 0 deletions src/components/button/download-logs-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Button from '@/components/button/button';
import { useP2P } from '@/contexts/P2PContext';
import { useOceanAccount } from '@/lib/use-ocean-account';
import { ComputeJob } from '@/types/jobs';
import { generateAuthTokenWithSmartAccount } from '@/utils/generateAuthToken';
import { useSignMessage, useSmartAccountClient } from '@account-kit/react';
import DownloadIcon from '@mui/icons-material/Download';
import JSZip from 'jszip';
import { useState } from 'react';
import { toast } from 'react-toastify';

interface DownloadLogsButtonProps {
job: ComputeJob;
}

export const DownloadLogsButton = ({ job }: DownloadLogsButtonProps) => {
const { getComputeResult, isReady } = useP2P();
const [isDownloading, setIsDownloading] = useState(false);
const { client } = useSmartAccountClient({ type: 'LightAccount' });
const { signMessageAsync } = useSignMessage({
client,
});
const { account } = useOceanAccount();

const handleDownload = async () => {
if (!isReady || isDownloading || !account?.address) return;

try {
const jobId = job.environment.split('-')[0] + '-' + job.jobId;

const authToken = await generateAuthTokenWithSmartAccount(job.peerId, account.address, signMessageAsync);

const logFiles = job.results.filter((result: any) => result.filename.includes('.log'));
const logPromises = logFiles.map((logFile: any) =>
getComputeResult(job.peerId, jobId, logFile.index, authToken, account.address)
);

const downloadedLogs = await Promise.all(logPromises);
setIsDownloading(true);

const zip = new JSZip();

downloadedLogs.forEach((logData, index) => {
if (logData instanceof Uint8Array) {
if (logData.byteLength !== 0) {
zip.file(logFiles[index].filename, logData);
}
}
});

const zipBlob = await zip.generateAsync({ type: 'blob' });

const url = URL.createObjectURL(zipBlob);
const link = document.createElement('a');
link.href = url;
link.download = `logs-${job.jobId}.zip`;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to download logs';
toast.error(errorMessage);
} finally {
setIsDownloading(false);
}
};

return (
<Button
autoLoading
color="accent1"
contentBefore={<DownloadIcon />}
disabled={!isReady}
onClick={handleDownload}
size="md"
variant="outlined"
>
Logs
</Button>
);
};
74 changes: 74 additions & 0 deletions src/components/button/download-result-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Button from '@/components/button/button';
import { useP2P } from '@/contexts/P2PContext';
import { useOceanAccount } from '@/lib/use-ocean-account';
import { ComputeJob } from '@/types/jobs';
import { generateAuthTokenWithSmartAccount } from '@/utils/generateAuthToken';
import { useSignMessage, useSmartAccountClient } from '@account-kit/react';
import DownloadIcon from '@mui/icons-material/Download';
import { useState } from 'react';
import { toast } from 'react-toastify';

interface DownloadResultButtonProps {
job: ComputeJob;
}

export const DownloadResultButton = ({ job }: DownloadResultButtonProps) => {
const { getComputeResult, isReady } = useP2P();
const [isDownloading, setIsDownloading] = useState(false);
const { client } = useSmartAccountClient({ type: 'LightAccount' });
const { signMessageAsync } = useSignMessage({
client,
});
const { account } = useOceanAccount();

const handleDownload = async () => {
if (!isReady || isDownloading || !account?.address) return;

try {
const jobId = job.environment.split('-')[0] + '-' + job.jobId;
setIsDownloading(true);
const archive = job.results.find((result: any) => result.filename.includes('.tar'));

const authToken = await generateAuthTokenWithSmartAccount(job.peerId, account.address, signMessageAsync);

const result = await getComputeResult(job.peerId, jobId, archive?.index, authToken, account.address);
if (result instanceof Uint8Array) {
if (result.byteLength === 0) {
console.log('Received empty response (0 bytes). Skipping download.');
return;
}

const blob = new Blob([result as any], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `outputs-${job.jobId}.tar`;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
URL.revokeObjectURL(url);
}
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to download result';
toast.error(errorMessage);
} finally {
setIsDownloading(false);
}
};

return (
<Button
autoLoading
color="accent1"
contentBefore={<DownloadIcon />}
disabled={!isReady}
onClick={handleDownload}
size="md"
variant="outlined"
>
Results
</Button>
);
};
26 changes: 26 additions & 0 deletions src/components/button/job-info-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { JobInfoModal } from '@/components/modal/job-info-modal';
import { ComputeJob } from '@/types/jobs';
import { useState } from 'react';
import Button from './button';

interface JobInfoButtonProps {
job: ComputeJob;
}

export const JobInfoButton = ({ job }: JobInfoButtonProps) => {
const [open, setOpen] = useState(false);

const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);

return (
<>
<Button color="accent1" variant="outlined" onClick={handleOpen}>
Job Info
</Button>
<JobInfoModal job={job} open={open} onClose={handleClose} />
</>
);
};

export default JobInfoButton;
63 changes: 63 additions & 0 deletions src/components/modal/job-info-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { DownloadLogsButton } from '@/components/button/download-logs-button';
import { DownloadResultButton } from '@/components/button/download-result-button';
import EnvironmentCard from '@/components/environment-card/environment-card';
import Modal from '@/components/modal/modal';
import { useProfileContext } from '@/context/profile-context';
import { ComputeJob } from '@/types/jobs';
import { Stack } from '@mui/material';
import classNames from 'classnames';
import { useEffect } from 'react';
import styles from './modal.module.css';

interface JobInfoModalProps {
job: ComputeJob | null;
open: boolean;
onClose: () => void;
}

export const JobInfoModal = ({ job, open, onClose }: JobInfoModalProps) => {
const { fetchNodeEnv, environment, nodeInfo } = useProfileContext();

useEffect(() => {
if (open && job?.environment) {
fetchNodeEnv(job.peerId, job.environment);
}
}, [open, fetchNodeEnv, job]);

if (!job) return null;

return (
<Modal isOpen={open} onClose={onClose} title="Job information" width="md">
<div
className={classNames(styles.root, styles['variant-glass-shaded'], styles['padding-md'], styles['radius-md'])}
>
<div className={styles.content}>
<Stack spacing={2}>
<div>
<div className={styles.label}>Job ID</div>
<div className={styles.value}>{job.jobId}</div>
</div>

{environment && (
<div>
<div className={styles.label} style={{ marginBottom: '8px' }}>
Environment
</div>
<EnvironmentCard key={environment.id} environment={environment} nodeInfo={nodeInfo} />
</div>
)}

<Stack
direction="row"
spacing={1}
sx={{ marginTop: '24px', paddingTop: '24px', borderTop: '1px solid var(--border-glass)' }}
>
<DownloadResultButton job={job} />
<DownloadLogsButton job={job} />
</Stack>
</Stack>
</div>
</div>
</Modal>
);
};
5 changes: 0 additions & 5 deletions src/components/profile/consumer-environments.module.css

This file was deleted.

24 changes: 0 additions & 24 deletions src/components/profile/consumer-environments.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions src/components/profile/consumer.profile-page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Container from '@/components/container/container';
import ConsumerEnvironments from '@/components/profile/consumer-environments';
import ConsumerJobs from '@/components/profile/consumer-jobs';
import ConsumerStats from '@/components/profile/consumer-stats';
import ProfileHeader from '@/components/profile/profile-header';
Expand All @@ -16,7 +15,6 @@ const ConsumerProfilePage = () => {
<div className="pageContentWrapper">
<ProfileHeader role="consumer" />
<ConsumerStats />
<ConsumerEnvironments />
<ConsumerJobs />
</div>
</Container>
Expand Down
8 changes: 4 additions & 4 deletions src/components/profile/owner-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const OwnerStats = () => {
totalNetworkJobs,
totalBenchmarkJobs,
ownerStatsPerEpoch,
activeNodes,
eligibleNodes,
totalNodes,
fetchOwnerStats,
fetchActiveNodes,
Expand Down Expand Up @@ -54,11 +54,11 @@ const OwnerStats = () => {
}}
/>
<Gauge
label="Active"
label="Eligible"
max={100}
min={0}
title="Active nodes"
value={totalNodes > 0 ? Number(((activeNodes / totalNodes) * 100).toFixed(1)) : 0}
title="Eligible nodes"
value={totalNodes > 0 ? Number(((eligibleNodes / totalNodes) * 100).toFixed(1)) : 0}
valueSuffix="%"
/>
</Card>
Expand Down
12 changes: 12 additions & 0 deletions src/components/table/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import InfoButton from '@/components/button/info-button';
import JobInfoButton from '@/components/button/job-info-button';
import { ComputeJob } from '@/types/jobs';
import { GPUPopularity, Node } from '@/types/nodes';
import { UnbanRequest } from '@/types/unban-requests';
Expand Down Expand Up @@ -251,6 +252,17 @@ export const jobsColumns: GridColDef<ComputeJob>[] = [
(operator) => operator.value === '=' || operator.value === '>' || operator.value === '<'
),
},
{
align: 'right',
field: 'actions',
filterable: false,
headerAlign: 'center',
headerName: 'Actions',
sortable: false,
renderCell: (params: GridRenderCellParams<ComputeJob>) => {
return <JobInfoButton job={params.row} />;
},
},
];

export const unbanRequestsColumns: GridColDef<UnbanRequest>[] = [
Expand Down
Loading