update loading logic, display remote and local hash

This commit is contained in:
bitful-pannul 2024-08-06 01:20:47 +03:00
parent fb815a7596
commit a5ed152747
4 changed files with 267 additions and 18 deletions

View File

@ -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" => {

View File

@ -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));
}

View File

@ -29,6 +29,9 @@ export default function AppPage() {
const [metadataUriContent, setMetadataUriContent] = useState<any>(null);
const [showAllVersions, setShowAllVersions] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [updateProgress, setUpdateProgress] = useState<number | null>(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 (
<section className="app-page">
@ -178,7 +206,7 @@ export default function AppPage() {
</div>
)}
</li>
<li><span>Update Available:</span> <span>{updateAvailable ? "Yes" : "No"}</span></li>
{/* <li><span>Update Available:</span> <span>{updateAvailable ? "Yes" : "No"}</span></li> */}
<li className="expandable-item">
<div className="item-header">
<span>~metadata-uri</span>
@ -285,7 +313,23 @@ export default function AppPage() {
{isInstalled ? (
<>
<button onClick={handleLaunch} className="primary"><FaPlay /> Launch</button>
<button onClick={handleUpdate} className="secondary"><FaSync /> Update</button>
{updateAvailable && (
<button
onClick={handleUpdate}
className={`secondary ${isUpdating ? 'updating' : ''}`}
disabled={!selectedMirror || isUpdating}
>
{isUpdating ? (
<>
<FaSpinner className="fa-spin" /> Updating...
</>
) : (
<>
<FaSync /> Update
</>
)}
</button>
)}
<button onClick={handleUninstall} className="secondary"><FaTrash /> Uninstall</button>
</>
) : (
@ -294,7 +338,7 @@ export default function AppPage() {
<select
value={selectedMirror || ''}
onChange={(e) => setSelectedMirror(e.target.value)}
disabled={isDownloading}
disabled={isDownloading || isUpdating}
>
<option value="" disabled>Select Mirror</option>
{Object.entries(mirrorStatuses).map(([mirror, status]) => (
@ -335,7 +379,8 @@ export default function AppPage() {
)}
</div>
{(isDownloading || isDownloaded) && (
{(isDownloading || isUpdating || isDownloaded) && (
<div className="progress-container">
<div className="progress-bar">
<div

View File

@ -16,7 +16,7 @@ interface AppsStore {
getApp: (id: string) => Promise<AppInfo>
checkMirror: (node: string) => Promise<MirrorCheckFile>
installApp: (app: AppInfo) => Promise<void>
updateApp: (app: AppInfo) => Promise<void>
updateApp: (app: AppInfo, downloadFrom: string) => Promise<void>
uninstallApp: (app: AppInfo) => Promise<void>
downloadApp: (app: AppInfo, downloadFrom: string) => Promise<void>
getCaps: (app: AppInfo) => Promise<PackageManifest>
@ -93,9 +93,18 @@ const useAppsStore = create<AppsStore>()(
await get().getApp(appId(app))
},
updateApp: async (app: AppInfo) => {
// Note: The backend doesn't have a specific update endpoint, so we might need to implement this differently
throw new Error('Update functionality not implemented')
updateApp: async (app: AppInfo, downloadFrom: string) => {
const res = await fetch(`${BASE_URL}/apps/${appId(app)}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ download_from: downloadFrom }),
});
if (res.status !== HTTP_STATUS.ACCEPTED) {
throw new Error(`Failed to update app: ${appId(app)}`);
}
},
uninstallApp: async (app: AppInfo) => {
@ -108,7 +117,7 @@ const useAppsStore = create<AppsStore>()(
downloadApp: async (app: AppInfo, downloadFrom: string) => {
const res = await fetch(`${BASE_URL}/apps/${appId(app)}/download`, {
method: 'PUT',
method: 'POST',
body: JSON.stringify({ download_from: downloadFrom }),
})
if (res.status !== HTTP_STATUS.CREATED) {
@ -116,6 +125,7 @@ const useAppsStore = create<AppsStore>()(
}
},
getCaps: async (app: AppInfo) => {
const res = await fetch(`${BASE_URL}/apps/${appId(app)}/caps`)
if (res.status === HTTP_STATUS.OK) {