mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 16:43:24 +03:00
app_store UI: mirror checks and info
This commit is contained in:
parent
746bc5f5f7
commit
14d862d3da
@ -1,4 +1,4 @@
|
||||
use crate::state::{PackageListing, State};
|
||||
use crate::state::{MirrorCheckFile, PackageListing, State};
|
||||
use crate::DownloadResponse;
|
||||
use kinode_process_lib::{
|
||||
http::{
|
||||
@ -7,6 +7,7 @@ use kinode_process_lib::{
|
||||
},
|
||||
println, Address, NodeId, PackageId, ProcessId, Request,
|
||||
};
|
||||
use kinode_process_lib::{SendError, SendErrorKind};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@ -22,6 +23,7 @@ pub fn init_frontend(our: &Address) {
|
||||
"/apps/:id/mirror",
|
||||
"/apps/:id/auto-update",
|
||||
"/apps/rebuild-index",
|
||||
"/mirrorcheck/:node",
|
||||
] {
|
||||
bind_http_path(path, true, false).expect("failed to bind http path");
|
||||
}
|
||||
@ -173,6 +175,7 @@ fn make_widget() -> String {
|
||||
/// - get detail about a specific app: GET /apps/:id
|
||||
/// - get capabilities for a specific downloaded app: GET /apps/:id/caps
|
||||
///
|
||||
/// - get online/offline mirrors for a listed app: GET /mirrorcheck/:node
|
||||
/// - install a downloaded app, download a listed app: POST /apps/:id
|
||||
/// - uninstall/delete a downloaded app: DELETE /apps/:id
|
||||
/// - update a downloaded app: PUT /apps/:id
|
||||
@ -218,6 +221,7 @@ fn gen_package_info(id: &PackageId, listing: &PackageListing) -> serde_json::Val
|
||||
None => false,
|
||||
},
|
||||
"metadata_hash": listing.metadata_hash,
|
||||
"metadata_uri": listing.metadata_uri,
|
||||
"metadata": listing.metadata,
|
||||
"state": match &listing.state {
|
||||
Some(state) => json!({
|
||||
@ -259,6 +263,54 @@ fn serve_paths(
|
||||
.collect();
|
||||
return Ok((StatusCode::OK, None, serde_json::to_vec(&all)?));
|
||||
}
|
||||
// GET online/offline mirrors for a listed app
|
||||
"/mirrorcheck/:node" => {
|
||||
if method != Method::GET {
|
||||
return Ok((
|
||||
StatusCode::METHOD_NOT_ALLOWED,
|
||||
None,
|
||||
format!("Invalid method {method} for {bound_path}").into_bytes(),
|
||||
));
|
||||
}
|
||||
let Some(node) = url_params.get("node") else {
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
None,
|
||||
format!("Missing node").into_bytes(),
|
||||
));
|
||||
};
|
||||
if let Err(SendError { kind, .. }) = Request::to((node, "net", "distro", "sys"))
|
||||
.body(b"checking your mirror status...")
|
||||
.send_and_await_response(3)
|
||||
.unwrap()
|
||||
{
|
||||
match kind {
|
||||
SendErrorKind::Timeout => {
|
||||
let check_reponse = MirrorCheckFile {
|
||||
node: node.to_string(),
|
||||
is_online: false,
|
||||
error: Some(format!("node {} timed out", node).to_string()),
|
||||
};
|
||||
return Ok((StatusCode::OK, None, serde_json::to_vec(&check_reponse)?));
|
||||
}
|
||||
SendErrorKind::Offline => {
|
||||
let check_reponse = MirrorCheckFile {
|
||||
node: node.to_string(),
|
||||
is_online: false,
|
||||
error: Some(format!("node {} is offline", node).to_string()),
|
||||
};
|
||||
return Ok((StatusCode::OK, None, serde_json::to_vec(&check_reponse)?));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let check_reponse = MirrorCheckFile {
|
||||
node: node.to_string(),
|
||||
is_online: true,
|
||||
error: None,
|
||||
};
|
||||
return Ok((StatusCode::OK, None, serde_json::to_vec(&check_reponse)?));
|
||||
}
|
||||
}
|
||||
// GET detail about a specific app
|
||||
// install an app: POST
|
||||
// update a downloaded app: PUT
|
||||
|
@ -49,6 +49,13 @@ pub struct MirroringFile {
|
||||
pub auto_update: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct MirrorCheckFile {
|
||||
pub node: NodeId,
|
||||
pub is_online: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct RequestedPackage {
|
||||
pub from: NodeId,
|
||||
|
@ -2,15 +2,6 @@ import { multicallAbi, kinomapAbi, mechAbi, KINOMAP, MULTICALL, KINO_ACCOUNT_IMP
|
||||
import { encodeFunctionData, encodePacked, stringToHex } from "viem";
|
||||
|
||||
export function encodeMulticalls(metadataUri: string, metadataHash: string) {
|
||||
const metadataUriCall = encodeFunctionData({
|
||||
abi: kinomapAbi,
|
||||
functionName: 'note',
|
||||
args: [
|
||||
encodePacked(["bytes"], [stringToHex("~metadata-uri")]),
|
||||
encodePacked(["bytes"], [stringToHex(metadataUri)]),
|
||||
]
|
||||
})
|
||||
|
||||
const metadataHashCall = encodeFunctionData({
|
||||
abi: kinomapAbi,
|
||||
functionName: 'note',
|
||||
@ -20,9 +11,18 @@ export function encodeMulticalls(metadataUri: string, metadataHash: string) {
|
||||
]
|
||||
})
|
||||
|
||||
const metadataUriCall = encodeFunctionData({
|
||||
abi: kinomapAbi,
|
||||
functionName: 'note',
|
||||
args: [
|
||||
encodePacked(["bytes"], [stringToHex("~metadata-uri")]),
|
||||
encodePacked(["bytes"], [stringToHex(metadataUri)]),
|
||||
]
|
||||
})
|
||||
|
||||
const calls = [
|
||||
{ target: KINOMAP, callData: metadataHashCall },
|
||||
{ target: KINOMAP, callData: metadataUriCall },
|
||||
{ target: KINOMAP, callData: metadataHashCall }
|
||||
];
|
||||
|
||||
const multicall = encodeFunctionData({
|
||||
|
@ -1,16 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { STORE_PATH, PUBLISH_PATH } from '../constants/path';
|
||||
import { ConnectButton } from '@rainbow-me/rainbowkit';
|
||||
import { FaHome } from "react-icons/fa";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<header className="app-header">
|
||||
<div className="header-left">
|
||||
<nav>
|
||||
<Link to={STORE_PATH} className={location.pathname === STORE_PATH ? 'active' : ''}>Home</Link>
|
||||
<button onClick={() => window.location.href = '/'}>
|
||||
<FaHome />
|
||||
</button>
|
||||
<Link to={STORE_PATH} className={location.pathname === STORE_PATH ? 'active' : ''}>Apps</Link>
|
||||
<Link to={PUBLISH_PATH} className={location.pathname === PUBLISH_PATH ? 'active' : ''}>Publish</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -108,13 +108,44 @@
|
||||
|
||||
/* App Page Styles */
|
||||
.app-page {
|
||||
max-width: 800px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 20px;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-title h2 {
|
||||
margin: 0;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.app-id {
|
||||
font-family: monospace;
|
||||
color: light-dark(var(--gray), var(--off-white));
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.app-description {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 20px;
|
||||
color: light-dark(var(--gray), var(--off-white));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.app-details {
|
||||
@ -127,66 +158,148 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-details-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
.info-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.app-details-list li {
|
||||
.info-section h3 {
|
||||
color: var(--orange);
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
background-color: light-dark(var(--tan), var(--maroon));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.detail-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid light-dark(var(--gray), var(--maroon));
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid light-dark(var(--gray-light), var(--gray));
|
||||
}
|
||||
|
||||
.app-details-list li:last-child {
|
||||
.detail-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: light-dark(var(--gray-light), var(--gray));
|
||||
}
|
||||
|
||||
.status-icon.installed,
|
||||
.status-icon.mirroring,
|
||||
.status-icon.auto-update {
|
||||
.installed,
|
||||
.verified,
|
||||
.approved,
|
||||
.mirroring,
|
||||
.auto-update {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.status-icon.not-installed,
|
||||
.status-icon.not-mirroring,
|
||||
.status-icon.no-auto-update {
|
||||
.not-installed,
|
||||
.not-verified,
|
||||
.not-approved,
|
||||
.not-mirroring,
|
||||
.no-auto-update {
|
||||
color: var(--ansi-red);
|
||||
}
|
||||
|
||||
.hash {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
word-break: break-all;
|
||||
color: light-dark(var(--gray), var(--off-white));
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.9em;
|
||||
background-color: light-dark(var(--gray-light), var(--gray));
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-button.active {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.toggle-button svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 10px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.app-actions button {
|
||||
.screenshot-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.app-screenshots {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.app-screenshot {
|
||||
max-width: 200px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
button,
|
||||
.external-link {
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.external-link:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
button svg,
|
||||
.external-link svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--gray);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.external-link {
|
||||
background-color: var(--blue);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* My Apps Page Styles */
|
||||
@ -504,30 +617,71 @@
|
||||
}
|
||||
|
||||
.mirrors-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
border: 1px solid var(--gray-light);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 10;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mirrors-list li {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--gray-light);
|
||||
.mirror-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mirrors-list li:last-child {
|
||||
border-bottom: none;
|
||||
.mirror-address {
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.mirrors-list li:hover {
|
||||
background-color: var(--gray-lighter);
|
||||
.check-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
margin-right: 10px;
|
||||
display: inline-flex !important;
|
||||
/* Force display */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.check-button svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--off-black);
|
||||
/* Ensure visibility */
|
||||
}
|
||||
|
||||
.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mirror-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.online {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.offline {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-left: 5px;
|
||||
font-size: 0.8em;
|
||||
color: #888;
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes } from "react-icons/fa";
|
||||
import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes, FaRocket, FaLink, FaChevronDown, FaChevronUp, FaShieldAlt } from "react-icons/fa";
|
||||
import useAppsStore from "../store";
|
||||
import { appId } from "../utils/app";
|
||||
import { MirrorCheckFile } from "../types/Apps";
|
||||
|
||||
export default function AppPage() {
|
||||
const { installApp, updateApp, uninstallApp, setMirroring, setAutoUpdate, apps } = useAppsStore();
|
||||
const { installApp, updateApp, uninstallApp, approveCaps, setMirroring, setAutoUpdate, checkMirror, apps } = useAppsStore();
|
||||
const { id } = useParams();
|
||||
const app = apps.find(a => appId(a) === id);
|
||||
const [showMetadata, setShowMetadata] = useState(true);
|
||||
const [showLocalInfo, setShowLocalInfo] = useState(true);
|
||||
const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: MirrorCheckFile | null }>({});
|
||||
|
||||
if (!app) {
|
||||
return <div className="app-page"><h4>App details not found for {id}</h4></div>;
|
||||
@ -17,54 +21,154 @@ export default function AppPage() {
|
||||
const handleUpdate = () => app && updateApp(app);
|
||||
const handleUninstall = () => app && uninstallApp(app);
|
||||
const handleMirror = () => app && setMirroring(app, !app.state?.mirroring);
|
||||
const handleApproveCaps = () => app && approveCaps(app);
|
||||
const handleAutoUpdate = () => app && setAutoUpdate(app, !app.state?.auto_update);
|
||||
const handleLaunch = () => {
|
||||
console.log("Launching app:", app.package);
|
||||
window.open(`/${app.package}:${app.publisher}`, '_blank');
|
||||
};
|
||||
|
||||
|
||||
const handleCheckMirror = (mirror: string) => {
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: null })); // Set to loading
|
||||
checkMirror(mirror)
|
||||
.then(status => setMirrorStatuses(prev => ({ ...prev, [mirror]: status })))
|
||||
.catch(error => {
|
||||
console.error(`Failed to check mirror ${mirror}:`, error);
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: { node: mirror, is_online: false, error: "Request failed" } }));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="app-page">
|
||||
<h2>{app.metadata?.name || app.package}</h2>
|
||||
<p className="app-description">{app.metadata?.description || "No description available"}</p>
|
||||
<div className="app-header">
|
||||
{app.metadata?.image && (
|
||||
<img src={app.metadata.image} alt={app.metadata?.name || app.package} className="app-icon" />
|
||||
)}
|
||||
<div className="app-title">
|
||||
<h2>{app.metadata?.name || app.package}</h2>
|
||||
<p className="app-id">{`${app.package}.${app.publisher}`}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="app-description">{app.metadata?.description || "No description available"}</div>
|
||||
|
||||
<div className="app-details">
|
||||
<div className="app-info">
|
||||
<ul className="app-details-list">
|
||||
<li><span>Version:</span> <span>{app.metadata?.properties?.current_version || "Unknown"}</span></li>
|
||||
<li><span>Developer:</span> <span>{app.publisher}</span></li>
|
||||
<li><span>Mirrors:</span> <span>{app.metadata?.properties?.mirrors?.length || 0}</span></li>
|
||||
<li>
|
||||
<span>Installed:</span>
|
||||
{app.installed ? <FaCheck className="status-icon installed" /> : <FaTimes className="status-icon not-installed" />}
|
||||
</li>
|
||||
<li>
|
||||
<span>Mirroring:</span>
|
||||
{app.state?.mirroring ? <FaCheck className="status-icon mirroring" /> : <FaTimes className="status-icon not-mirroring" />}
|
||||
</li>
|
||||
<li>
|
||||
<span>Auto-Update:</span>
|
||||
{app.state?.auto_update ? <FaCheck className="status-icon auto-update" /> : <FaTimes className="status-icon no-auto-update" />}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="info-section">
|
||||
<h3 onClick={() => setShowMetadata(!showMetadata)}>
|
||||
Metadata {showMetadata ? <FaChevronUp /> : <FaChevronDown />}
|
||||
</h3>
|
||||
{showMetadata && (
|
||||
<ul className="detail-list">
|
||||
<li><span>Version:</span> <span>{app.metadata?.properties?.current_version || "Unknown"}</span></li>
|
||||
<li><span>~metadata-uri</span> <span className="hash">{app.metadata_uri}</span></li>
|
||||
<li><span>~metadata-hash</span> <span className="hash">{app.metadata_hash}</span></li>
|
||||
<li className="mirrors-list">
|
||||
<span>Mirrors:</span>
|
||||
<ul>
|
||||
{app.metadata?.properties?.mirrors?.map((mirror) => (
|
||||
<li key={mirror} className="mirror-item">
|
||||
<span className="mirror-address">{mirror}</span>
|
||||
<button
|
||||
onClick={() => handleCheckMirror(mirror)}
|
||||
className="check-button"
|
||||
title="Check if mirror is online"
|
||||
>
|
||||
<FaSync className={mirrorStatuses[mirror] === null ? 'spinning' : ''} />
|
||||
</button>
|
||||
{mirrorStatuses[mirror] && (
|
||||
<span className="mirror-status">
|
||||
{mirrorStatuses[mirror]?.is_online ? (
|
||||
<><FaCheck className="online" /> <span className="online">Online</span></>
|
||||
) : (
|
||||
<>
|
||||
<FaTimes className="offline" />
|
||||
<span className="offline">Offline</span>
|
||||
{mirrorStatuses[mirror]?.error && (
|
||||
<span className="error-message">
|
||||
({mirrorStatuses[mirror]?.error})
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="info-section">
|
||||
<h3 onClick={() => setShowLocalInfo(!showLocalInfo)}>
|
||||
Local Information {showLocalInfo ? <FaChevronUp /> : <FaChevronDown />}
|
||||
</h3>
|
||||
{showLocalInfo && (
|
||||
<ul className="detail-list">
|
||||
<li>
|
||||
<span>Installed:</span>
|
||||
<span className="status-icon">{app.installed ? <FaCheck className="installed" /> : <FaTimes className="not-installed" />}</span>
|
||||
</li>
|
||||
<li><span>Installed Version:</span> <span>{app.state?.our_version || "Not installed"}</span></li>
|
||||
<li>
|
||||
<span>Verified:</span>
|
||||
<span className="status-icon">{app.state?.verified ? <FaCheck className="verified" /> : <FaTimes className="not-verified" />}</span>
|
||||
</li>
|
||||
<li><span>License:</span> <span>{app.metadata?.properties?.license || "Not specified"}</span></li>
|
||||
<li>
|
||||
<span>Capabilities Approved:</span>
|
||||
<button onClick={handleApproveCaps} className={`toggle-button ${app.state?.caps_approved ? 'active' : ''}`}>
|
||||
{app.state?.caps_approved ? <FaCheck /> : <FaShieldAlt />}
|
||||
{app.state?.caps_approved ? "Approved" : "Approve Caps"}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<span>Mirroring:</span>
|
||||
<button onClick={handleMirror} className={`toggle-button ${app.state?.mirroring ? 'active' : ''}`}>
|
||||
<FaMagnet />
|
||||
{app.state?.mirroring ? "Mirroring" : "Start Mirroring"}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<span>Auto-Update:</span>
|
||||
<button onClick={handleAutoUpdate} className={`toggle-button ${app.state?.auto_update ? 'active' : ''}`}>
|
||||
<FaCog />
|
||||
{app.state?.auto_update ? "Auto-Update On" : "Enable Auto-Update"}
|
||||
</button>
|
||||
</li>
|
||||
<li><span>Manifest Hash:</span> <span className="hash">{app.state?.manifest_hash || "N/A"}</span></li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-actions">
|
||||
{app.installed ? (
|
||||
<>
|
||||
<button onClick={handleLaunch} className="primary"><FaRocket /> Launch</button>
|
||||
<button onClick={handleUpdate} className="secondary"><FaSync /> Update</button>
|
||||
<button onClick={handleUninstall} className="secondary"><FaTrash /> Uninstall</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={handleInstall}><FaDownload /> Install</button>
|
||||
<button onClick={handleInstall} className="primary"><FaDownload /> Install</button>
|
||||
)}
|
||||
{app.metadata?.external_url && (
|
||||
<a href={app.metadata.external_url} target="_blank" rel="noopener noreferrer" className="external-link">
|
||||
<FaLink /> External Link
|
||||
</a>
|
||||
)}
|
||||
<button onClick={handleMirror} className="secondary">
|
||||
<FaMagnet /> {app.state?.mirroring ? "Stop Mirroring" : "Start Mirroring"}
|
||||
</button>
|
||||
<button onClick={handleAutoUpdate} className="secondary">
|
||||
<FaCog /> {app.state?.auto_update ? "Disable Auto-Update" : "Enable Auto-Update"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{app.metadata?.properties?.screenshots && (
|
||||
<div className="app-screenshots">
|
||||
{app.metadata.properties.screenshots.map((screenshot, index) => (
|
||||
<img key={index} src={screenshot} alt={`Screenshot ${index + 1}`} className="app-screenshot" />
|
||||
))}
|
||||
<h3>Screenshots</h3>
|
||||
<div className="screenshot-container">
|
||||
{app.metadata.properties.screenshots.map((screenshot, index) => (
|
||||
<img key={index} src={screenshot} alt={`Screenshot ${index + 1}`} className="app-screenshot" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
@ -1,76 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { FaUpload } from "react-icons/fa";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import useAppsStore from "../store";
|
||||
import { PUBLISH_PATH } from "../constants/path";
|
||||
import { appId } from "../utils/app";
|
||||
|
||||
export default function MyAppsPage() {
|
||||
const { apps, getApps } = useAppsStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
getApps();
|
||||
}, [getApps]);
|
||||
|
||||
const filteredApps = apps.filter((app) =>
|
||||
app.package.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const categorizedApps = {
|
||||
installed: filteredApps.filter(app => app.installed),
|
||||
downloaded: filteredApps.filter(app => !app.installed && app.state),
|
||||
available: filteredApps.filter(app => !app.state)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-apps-page">
|
||||
<div className="my-apps-header">
|
||||
<h1>My Packages</h1>
|
||||
<button className="publish-button" onClick={() => navigate(PUBLISH_PATH)}>
|
||||
<FaUpload className="mr-2" />
|
||||
Publish Package
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search packages..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
|
||||
<div className="apps-list">
|
||||
{Object.entries(categorizedApps).map(([category, apps]) => (
|
||||
apps.length > 0 && (
|
||||
<div key={category} className="app-category">
|
||||
<h2>{category.charAt(0).toUpperCase() + category.slice(1)}</h2>
|
||||
{apps.map((app) => (
|
||||
<AppEntry key={app.package} app={app} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface AppEntryProps {
|
||||
app: AppInfo;
|
||||
}
|
||||
|
||||
const AppEntry: React.FC<AppEntryProps> = ({ app }) => {
|
||||
return (
|
||||
<Link to={`/app/${appId(app)}`} className="app-entry">
|
||||
<h3>{app.metadata?.name || app.package}</h3>
|
||||
<p>{app.metadata?.description || "No description available"}</p>
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { AppInfo, PackageManifest } from '../types/Apps'
|
||||
import { AppInfo, MirrorCheckFile, PackageManifest } from '../types/Apps'
|
||||
import { HTTP_STATUS } from '../constants/http'
|
||||
import { appId } from '../utils/app'
|
||||
|
||||
@ -10,6 +10,7 @@ interface AppsStore {
|
||||
apps: AppInfo[]
|
||||
getApps: () => Promise<void>
|
||||
getApp: (id: string) => Promise<AppInfo>
|
||||
checkMirror: (node: string) => Promise<MirrorCheckFile>
|
||||
installApp: (app: AppInfo) => Promise<void>
|
||||
updateApp: (app: AppInfo) => Promise<void>
|
||||
uninstallApp: (app: AppInfo) => Promise<void>
|
||||
@ -44,6 +45,14 @@ const useAppsStore = create<AppsStore>()(
|
||||
throw new Error(`Failed to get app: ${id}`)
|
||||
},
|
||||
|
||||
checkMirror: async (node: string) => {
|
||||
const res = await fetch(`${BASE_URL}/mirrorcheck/${node}`)
|
||||
if (res.status === HTTP_STATUS.OK) {
|
||||
return await res.json()
|
||||
}
|
||||
throw new Error(`Failed to check mirror status for node: ${node}`)
|
||||
},
|
||||
|
||||
installApp: async (app: AppInfo) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, { method: 'POST' })
|
||||
if (res.status !== HTTP_STATUS.CREATED) {
|
||||
@ -91,6 +100,7 @@ const useAppsStore = create<AppsStore>()(
|
||||
if (res.status !== HTTP_STATUS.OK) {
|
||||
throw new Error(`Failed to approve caps for app: ${appId(app)}`)
|
||||
}
|
||||
await get().getApp(appId(app))
|
||||
},
|
||||
|
||||
setMirroring: async (app: AppInfo, mirroring: boolean) => {
|
||||
|
@ -10,11 +10,18 @@ export interface AppListing {
|
||||
package: string
|
||||
publisher: string
|
||||
metadata_hash: string
|
||||
metadata_uri: string
|
||||
metadata?: OnchainPackageMetadata
|
||||
installed: boolean
|
||||
state?: PackageState
|
||||
}
|
||||
|
||||
export interface MirrorCheckFile {
|
||||
node: string;
|
||||
is_online: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface Erc721Properties {
|
||||
package_name: string;
|
||||
publisher: string;
|
||||
|
Loading…
Reference in New Issue
Block a user