mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-22 16:11:38 +03:00
update loading logic, display remote and local hash
This commit is contained in:
parent
fb815a7596
commit
a5ed152747
@ -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" => {
|
||||
|
@ -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));
|
||||
}
|
@ -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
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user