From fb815a7596129dd57ac539298ca6d06d9cf3907e Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Mon, 5 Aug 2024 20:17:50 +0300 Subject: [PATCH 1/3] app_store UI: show metadata uri json on toggle, latest version --- kinode/packages/app_store/ui/src/index.css | 43 +++++++++++ .../app_store/ui/src/pages/AppPage.tsx | 72 ++++++++++++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/kinode/packages/app_store/ui/src/index.css b/kinode/packages/app_store/ui/src/index.css index af8a7390..4ec35fdf 100644 --- a/kinode/packages/app_store/ui/src/index.css +++ b/kinode/packages/app_store/ui/src/index.css @@ -692,4 +692,47 @@ button svg, overflow-x: auto; white-space: pre-wrap; word-break: break-all; +} + + +.hash { + font-family: monospace; + word-break: break-all; +} + +.expandable-item { + background-color: light-dark(var(--tan), var(--maroon)); + border-radius: 4px; + overflow: hidden; +} + +.item-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; +} + +.dropdown-toggle { + background: none; + border: none; + cursor: pointer; + color: var(--blue); + padding: 5px; +} + +.json-container { + max-height: 300px; + overflow-y: auto; + background-color: light-dark(var(--off-white), var(--off-black)); +} + +.json-display { + font-family: monospace; + font-size: 12px; + white-space: pre-wrap; + word-wrap: break-word; + padding: 10px; + margin: 0; + color: light-dark(var(--off-black), var(--off-white)); } \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/pages/AppPage.tsx b/kinode/packages/app_store/ui/src/pages/AppPage.tsx index 24a77b96..c23ed606 100644 --- a/kinode/packages/app_store/ui/src/pages/AppPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/AppPage.tsx @@ -21,6 +21,15 @@ export default function AppPage() { const [showCaps, setShowCaps] = useState(false); const [localProgress, setLocalProgress] = useState(null); + // split these out a bit, this page is getting slightly too heavy + const [latestVersion, setLatestVersion] = useState(null); + const [updateAvailable, setUpdateAvailable] = useState(false); + + const [showMetadataUri, setShowMetadataUri] = useState(false); + const [metadataUriContent, setMetadataUriContent] = useState(null); + const [showAllVersions, setShowAllVersions] = useState(false); + + useEffect(() => { if (app) { checkMirrors(); @@ -28,6 +37,24 @@ export default function AppPage() { } }, [app]); + useEffect(() => { + if (app && app.metadata_uri) { + fetch(app.metadata_uri) + .then(response => response.json()) + .then(data => setMetadataUriContent(data)) + .catch(error => console.error('Error fetching metadata URI:', error)); + } + }, [app]); + + useEffect(() => { + if (app && app.metadata?.properties?.code_hashes) { + const versions = Object.keys(app.metadata.properties.code_hashes); + const latest = versions[versions.length - 1]; + setLatestVersion(latest); + setUpdateAvailable(app.state?.our_version !== latest); + } + }, [app]); + if (!app) { return

App details not found for {id}

; } @@ -129,9 +156,50 @@ export default function AppPage() { {showMetadata && (
    -
  • Version: {app.metadata?.properties?.current_version || "Unknown"}
  • -
  • ~metadata-uri {app.metadata_uri}
  • +
  • + Current Version: {app.state?.our_version || "Not installed"} +
  • +
  • +
    + Latest Version: + {latestVersion || "Unknown"} + +
    + {showAllVersions && app.metadata?.properties?.code_hashes && ( +
    +
    +                        {JSON.stringify(app.metadata.properties.code_hashes, null, 2)}
    +                      
    +
    + )} +
  • +
  • Update Available: {updateAvailable ? "Yes" : "No"}
  • +
  • +
    + ~metadata-uri + {app.metadata_uri} + +
    + {showMetadataUri && metadataUriContent && ( +
    +
    +                        {JSON.stringify(metadataUriContent, null, 2)}
    +                      
    +
    + )} +
  • ~metadata-hash {app.metadata_hash}
  • +
  • Mirrors:
      From a5ed152747d94e9af815b31df7e203ad90957722 Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Tue, 6 Aug 2024 01:20:47 +0300 Subject: [PATCH 2/3] update loading logic, display remote and local hash --- .../app_store/app_store/src/http_api.rs | 70 ++++++++++ kinode/packages/app_store/ui/src/index.css | 126 +++++++++++++++++- .../app_store/ui/src/pages/AppPage.tsx | 69 ++++++++-- .../packages/app_store/ui/src/store/index.ts | 20 ++- 4 files changed, 267 insertions(+), 18 deletions(-) diff --git a/kinode/packages/app_store/app_store/src/http_api.rs b/kinode/packages/app_store/app_store/src/http_api.rs index 74337a18..a0b9c75b 100644 --- a/kinode/packages/app_store/app_store/src/http_api.rs +++ b/kinode/packages/app_store/app_store/src/http_api.rs @@ -21,6 +21,7 @@ pub fn init_frontend(our: &Address) { "/apps/:id", "/apps/:id/download", "/apps/:id/install", + "/apps/:id/update", "/apps/:id/caps", "/apps/:id/mirror", "/apps/:id/auto-update", @@ -411,6 +412,75 @@ fn serve_paths( )), } } + // POST /apps/:id/update + // update a downloaded app + "/apps/:id/update" => { + let Ok(package_id) = get_package_id(url_params) else { + return Ok(( + StatusCode::BAD_REQUEST, + None, + format!("Missing id").into_bytes(), + )); + }; + + match method { + Method::POST => { + let pkg_listing: &PackageListing = state + .packages + .get(&package_id) + .ok_or(anyhow::anyhow!("No package"))?; + + let body = crate::get_blob() + .ok_or(anyhow::anyhow!("missing blob"))? + .bytes; + let body_json: serde_json::Value = + serde_json::from_slice(&body).unwrap_or_default(); + + let download_from = body_json + .get("download_from") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .or_else(|| { + pkg_listing + .metadata + .as_ref()? + .properties + .mirrors + .first() + .map(|m| m.to_string()) + }) + .ok_or_else(|| anyhow::anyhow!("No download_from specified!"))?; + + let desired_version_hash = body_json + .get("version") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + match crate::start_download( + state, + package_id, + download_from, + false, // Don't mirror during update + pkg_listing.state.as_ref().map_or(false, |s| s.auto_update), + desired_version_hash, + ) { + DownloadResponse::Started => { + Ok((StatusCode::ACCEPTED, None, format!("Updating").into_bytes())) + } + other => Ok(( + StatusCode::SERVICE_UNAVAILABLE, + None, + format!("Failed to update: {other:?}").into_bytes(), + )), + } + } + _ => Ok(( + StatusCode::METHOD_NOT_ALLOWED, + None, + format!("Invalid method {method} for {bound_path}").into_bytes(), + )), + } + } // POST /apps/:id/install // install a downloaded app "/apps/:id/install" => { diff --git a/kinode/packages/app_store/ui/src/index.css b/kinode/packages/app_store/ui/src/index.css index 4ec35fdf..5e6ea004 100644 --- a/kinode/packages/app_store/ui/src/index.css +++ b/kinode/packages/app_store/ui/src/index.css @@ -63,7 +63,39 @@ .app-actions { display: flex; flex-direction: column; - gap: 1rem; + gap: 10px; + min-width: 200px; +} + +.app-actions-buttons { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.app-actions-buttons button { + flex: 1 0 auto; + min-width: 120px; + /* Ensure buttons have a minimum width */ +} + +.mirror-selection { + width: 100%; + margin-bottom: 10px; +} + +.mirror-selection select { + width: 100%; + padding: 8px; + border-radius: 4px; + border: 1px solid var(--gray); + background-color: light-dark(var(--off-white), var(--off-black)); + color: light-dark(var(--off-black), var(--off-white)); +} + +.external-link { + display: inline-block; + margin-top: 10px; } @media (max-width: 768px) { @@ -735,4 +767,96 @@ button svg, padding: 10px; margin: 0; color: light-dark(var(--off-black), var(--off-white)); +} + +/* Add these new styles */ +.app-actions button { + transition: all 0.3s ease; +} + +.app-actions button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.app-actions button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.updating { + background-color: var(--yellow); + color: var(--off-black); +} + +.progress-container { + margin-top: 1rem; +} + +.progress-bar { + height: 10px; + background-color: var(--off-white); + border-radius: 5px; + overflow: hidden; +} + +.progress { + height: 100%; + background-color: var(--blue); + transition: width 0.3s ease; +} + +.progress-percentage { + text-align: center; + font-size: 0.8rem; + margin-top: 0.25rem; +} + +.expandable-item { + background-color: light-dark(var(--tan), var(--maroon)); + border-radius: 4px; + overflow: hidden; + transition: all 0.3s ease; +} + +.expandable-item:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.item-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + cursor: pointer; +} + +.dropdown-toggle { + background: none; + border: none; + cursor: pointer; + color: var(--blue); + padding: 5px; + transition: transform 0.3s ease; +} + +.dropdown-toggle:hover { + transform: scale(1.1); +} + +.json-container { + max-height: 300px; + overflow-y: auto; + background-color: light-dark(var(--off-white), var(--off-black)); + padding: 10px; + border-top: 1px solid var(--gray); +} + +.json-display { + font-family: monospace; + font-size: 12px; + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; + color: light-dark(var(--off-black), var(--off-white)); } \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/pages/AppPage.tsx b/kinode/packages/app_store/ui/src/pages/AppPage.tsx index c23ed606..32632ff9 100644 --- a/kinode/packages/app_store/ui/src/pages/AppPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/AppPage.tsx @@ -29,6 +29,9 @@ export default function AppPage() { const [metadataUriContent, setMetadataUriContent] = useState(null); const [showAllVersions, setShowAllVersions] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + const [updateProgress, setUpdateProgress] = useState(null); + useEffect(() => { if (app) { @@ -48,10 +51,12 @@ export default function AppPage() { useEffect(() => { if (app && app.metadata?.properties?.code_hashes) { - const versions = Object.keys(app.metadata.properties.code_hashes); - const latest = versions[versions.length - 1]; - setLatestVersion(latest); - setUpdateAvailable(app.state?.our_version !== latest); + const versions = Object.entries(app.metadata.properties.code_hashes); + if (versions.length > 0) { + const [latestVersion, latestHash] = versions[versions.length - 1]; + setLatestVersion(latestVersion); + setUpdateAvailable(app.state?.our_version !== latestHash); + } } }, [app]); @@ -101,6 +106,28 @@ export default function AppPage() { } }; + const handleUpdate = async () => { + if (selectedMirror) { + setError(null); + setIsUpdating(true); + setUpdateProgress(0); + try { + await updateApp(app, selectedMirror); + setUpdateProgress(100); + setTimeout(() => { + setIsUpdating(false); + setUpdateProgress(null); + getApp(app.package); // Refresh app data after update + }, 3000); + } catch (error) { + console.error('Update failed:', error); + setError(`Update failed: ${error instanceof Error ? error.message : String(error)}`); + setIsUpdating(false); + setUpdateProgress(null); + } + } + }; + const handleInstall = async () => { setIsInstalling(true); setError(null); @@ -118,7 +145,6 @@ export default function AppPage() { } }; - const handleUpdate = () => updateApp(app); const handleUninstall = () => uninstallApp(app); const handleMirror = () => setMirroring(app, !app.state?.mirroring); const handleAutoUpdate = () => setAutoUpdate(app, !app.state?.auto_update); @@ -130,9 +156,11 @@ export default function AppPage() { const isDownloaded = app.state !== null; const isInstalled = app.installed; - const progressPercentage = localProgress !== null - ? localProgress - : isDownloaded ? 100 : 0; + const progressPercentage = updateProgress !== null + ? updateProgress + : localProgress !== null + ? localProgress + : isDownloaded ? 100 : 0; return (
      @@ -178,7 +206,7 @@ export default function AppPage() { )} -
    • Update Available: {updateAvailable ? "Yes" : "No"}
    • + {/*
    • Update Available: {updateAvailable ? "Yes" : "No"}
    • */}
    • ~metadata-uri @@ -285,7 +313,23 @@ export default function AppPage() { {isInstalled ? ( <> - + {updateAvailable && ( + + )} ) : ( @@ -294,7 +338,7 @@ export default function AppPage() { setSelectedMirror(e.target.value)} + disabled={isUpdating} + > + + {Object.entries(mirrorStatuses).map(([mirror, status]) => ( + + ))} + +
      + + )}