mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 00:21:38 +03:00
Merge pull request #587 from kinode-dao/bp/uninstalls
app_store UI: enable uninstalling local (unlisted) packages
This commit is contained in:
commit
a33390a27f
@ -2,13 +2,13 @@ import React from "react";
|
||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
|
||||
import Header from "./components/Header";
|
||||
import { APP_DETAILS_PATH, DOWNLOAD_PATH, MY_DOWNLOADS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
|
||||
import { APP_DETAILS_PATH, DOWNLOAD_PATH, MY_APPS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
|
||||
|
||||
import StorePage from "./pages/StorePage";
|
||||
import AppPage from "./pages/AppPage";
|
||||
import DownloadPage from "./pages/DownloadPage";
|
||||
import PublishPage from "./pages/PublishPage";
|
||||
import MyDownloadsPage from "./pages/MyDownloadsPage";
|
||||
import MyAppsPage from "./pages/MyAppsPage";
|
||||
|
||||
|
||||
const BASE_URL = import.meta.env.BASE_URL;
|
||||
@ -22,7 +22,7 @@ function App() {
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path={STORE_PATH} element={<StorePage />} />
|
||||
<Route path={MY_DOWNLOADS_PATH} element={<MyDownloadsPage />} />
|
||||
<Route path={MY_APPS_PATH} element={<MyAppsPage />} />
|
||||
<Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />
|
||||
<Route path={PUBLISH_PATH} element={<PublishPage />} />
|
||||
<Route path={`${DOWNLOAD_PATH}/:id`} element={<DownloadPage />} />
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { STORE_PATH, PUBLISH_PATH, MY_DOWNLOADS_PATH } from '../constants/path';
|
||||
import { STORE_PATH, PUBLISH_PATH, MY_APPS_PATH } from '../constants/path';
|
||||
import { ConnectButton } from '@rainbow-me/rainbowkit';
|
||||
import { FaHome } from "react-icons/fa";
|
||||
|
||||
@ -9,12 +9,12 @@ const Header: React.FC = () => {
|
||||
<header className="app-header">
|
||||
<div className="header-left">
|
||||
<nav>
|
||||
<button onClick={() => window.location.href = '/'}>
|
||||
<button onClick={() => window.location.href = '/'} className="home-button">
|
||||
<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>
|
||||
<Link to={MY_DOWNLOADS_PATH} className={location.pathname === MY_DOWNLOADS_PATH ? 'active' : ''}>My Downloads</Link>
|
||||
<Link to={MY_APPS_PATH} className={location.pathname === MY_APPS_PATH ? 'active' : ''}>My Apps</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
|
@ -2,4 +2,4 @@ export const STORE_PATH = '/';
|
||||
export const PUBLISH_PATH = '/publish';
|
||||
export const APP_DETAILS_PATH = '/app';
|
||||
export const DOWNLOAD_PATH = '/download';
|
||||
export const MY_DOWNLOADS_PATH = '/my-downloads';
|
||||
export const MY_APPS_PATH = '/my-apps';
|
||||
|
@ -136,6 +136,8 @@ td {
|
||||
.app-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
min-width: 64px;
|
||||
min-height: 64px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
@ -348,6 +350,13 @@ td {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.home-button {
|
||||
min-width: 48px;
|
||||
min-height: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.app-screenshot {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
|
@ -3,15 +3,41 @@ import { FaFolder, FaFile, FaChevronLeft, FaSync, FaRocket, FaSpinner, FaCheck,
|
||||
import useAppsStore from "../store";
|
||||
import { DownloadItem, PackageManifest, PackageState } from "../types/Apps";
|
||||
|
||||
export default function MyDownloadsPage() {
|
||||
const { fetchDownloads, fetchDownloadsForApp, startMirroring, stopMirroring, installApp, removeDownload, fetchInstalled, installed } = useAppsStore();
|
||||
// Core packages that cannot be uninstalled
|
||||
const CORE_PACKAGES = [
|
||||
"app_store:sys",
|
||||
"contacts:sys",
|
||||
"kino_updates:sys",
|
||||
"terminal:sys",
|
||||
"chess:sys",
|
||||
"kns_indexer:sys",
|
||||
"settings:sys",
|
||||
"homepage:sys"
|
||||
];
|
||||
|
||||
export default function MyAppsPage() {
|
||||
const {
|
||||
fetchDownloads,
|
||||
fetchDownloadsForApp,
|
||||
startMirroring,
|
||||
stopMirroring,
|
||||
installApp,
|
||||
removeDownload,
|
||||
fetchInstalled,
|
||||
installed,
|
||||
uninstallApp
|
||||
} = useAppsStore();
|
||||
|
||||
const [currentPath, setCurrentPath] = useState<string[]>([]);
|
||||
const [items, setItems] = useState<DownloadItem[]>([]);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [isUninstalling, setIsUninstalling] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showCapApproval, setShowCapApproval] = useState(false);
|
||||
const [manifest, setManifest] = useState<PackageManifest | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<DownloadItem | null>(null);
|
||||
const [showUninstallConfirm, setShowUninstallConfirm] = useState(false);
|
||||
const [appToUninstall, setAppToUninstall] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadItems();
|
||||
@ -33,6 +59,35 @@ export default function MyDownloadsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const initiateUninstall = (app: any) => {
|
||||
const packageId = `${app.package_id.package_name}:${app.package_id.publisher_node}`;
|
||||
if (CORE_PACKAGES.includes(packageId)) {
|
||||
setError("Cannot uninstall core system packages");
|
||||
return;
|
||||
}
|
||||
setAppToUninstall(app);
|
||||
setShowUninstallConfirm(true);
|
||||
};
|
||||
|
||||
const handleUninstall = async () => {
|
||||
if (!appToUninstall) return;
|
||||
setIsUninstalling(true);
|
||||
const packageId = `${appToUninstall.package_id.package_name}:${appToUninstall.package_id.publisher_node}`;
|
||||
try {
|
||||
await uninstallApp(packageId);
|
||||
await fetchInstalled();
|
||||
await loadItems();
|
||||
setShowUninstallConfirm(false);
|
||||
setAppToUninstall(null);
|
||||
} catch (error) {
|
||||
console.error('Uninstallation failed:', error);
|
||||
setError(`Uninstallation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
} finally {
|
||||
setIsUninstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const navigateToItem = (item: DownloadItem) => {
|
||||
if (item.Dir) {
|
||||
setCurrentPath([...currentPath, item.Dir.name]);
|
||||
@ -85,7 +140,6 @@ export default function MyDownloadsPage() {
|
||||
|
||||
if (!versionHash) throw new Error('Invalid file name format');
|
||||
|
||||
// Construct packageId by combining currentPath and remaining parts of the filename
|
||||
const packageId = [...currentPath, ...parts].join(':');
|
||||
|
||||
await installApp(packageId, versionHash);
|
||||
@ -121,8 +175,48 @@ export default function MyDownloadsPage() {
|
||||
|
||||
return (
|
||||
<div className="downloads-page">
|
||||
<h2>Downloads</h2>
|
||||
<h2>My Apps</h2>
|
||||
|
||||
{/* Installed Apps Section */}
|
||||
<div className="file-explorer">
|
||||
<h3>Installed Apps</h3>
|
||||
<table className="downloads-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package ID</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.values(installed).map((app) => {
|
||||
const packageId = `${app.package_id.package_name}:${app.package_id.publisher_node}`;
|
||||
const isCore = CORE_PACKAGES.includes(packageId);
|
||||
return (
|
||||
<tr key={packageId}>
|
||||
<td>{packageId}</td>
|
||||
<td>
|
||||
{isCore ? (
|
||||
<span className="core-package">Core Package</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => initiateUninstall(app)}
|
||||
disabled={isUninstalling}
|
||||
>
|
||||
{isUninstalling ? <FaSpinner className="fa-spin" /> : <FaTrash />}
|
||||
Uninstall
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Downloads Section */}
|
||||
<div className="file-explorer">
|
||||
<h3>Downloads</h3>
|
||||
<div className="path-navigation">
|
||||
{currentPath.length > 0 && (
|
||||
<button onClick={navigateUp} className="navigate-up">
|
||||
@ -172,7 +266,8 @@ export default function MyDownloadsPage() {
|
||||
)}
|
||||
{isFile && isInstalled && (
|
||||
<FaCheck className="installed" />
|
||||
)} </td>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@ -186,6 +281,45 @@ export default function MyDownloadsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Uninstall Confirmation Modal */}
|
||||
{showUninstallConfirm && appToUninstall && (
|
||||
<div className="cap-approval-popup">
|
||||
<div className="cap-approval-content">
|
||||
<h3>Confirm Uninstall</h3>
|
||||
<div className="warning-message">
|
||||
Are you sure you want to uninstall this app?
|
||||
</div>
|
||||
<div className="package-info">
|
||||
<strong>Package ID:</strong> {`${appToUninstall.package_id.package_name}:${appToUninstall.package_id.publisher_node}`}
|
||||
</div>
|
||||
{appToUninstall.metadata?.name && (
|
||||
<div className="package-info">
|
||||
<strong>Name:</strong> {appToUninstall.metadata.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="approval-buttons">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUninstallConfirm(false);
|
||||
setAppToUninstall(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUninstall}
|
||||
disabled={isUninstalling}
|
||||
className="danger"
|
||||
>
|
||||
{isUninstalling ? <FaSpinner className="fa-spin" /> : 'Confirm Uninstall'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{showCapApproval && manifest && (
|
||||
<div className="cap-approval-popup">
|
||||
<div className="cap-approval-content">
|
@ -17,7 +17,7 @@ The format is "/" + "process_name:package_name:publisher_node"
|
||||
const BASE_URL = `/main:app_store:sys`;
|
||||
|
||||
// This is the proxy URL, it must match the node you are developing against
|
||||
const PROXY_URL = (process.env.VITE_NODE_URL || 'http://127.0.0.1:8080').replace('localhost', '127.0.0.1');
|
||||
const PROXY_URL = (process.env.VITE_NODE_URL || 'http://127.0.0.1:8080');
|
||||
|
||||
console.log('process.env.VITE_NODE_URL', process.env.VITE_NODE_URL, PROXY_URL);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user