Merge pull request #587 from kinode-dao/bp/uninstalls

app_store UI: enable uninstalling local (unlisted) packages
This commit is contained in:
bitful-pannul 2024-10-30 16:45:59 +01:00 committed by GitHub
commit a33390a27f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 156 additions and 13 deletions

View File

@ -2,13 +2,13 @@ import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Header from "./components/Header"; 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 StorePage from "./pages/StorePage";
import AppPage from "./pages/AppPage"; import AppPage from "./pages/AppPage";
import DownloadPage from "./pages/DownloadPage"; import DownloadPage from "./pages/DownloadPage";
import PublishPage from "./pages/PublishPage"; import PublishPage from "./pages/PublishPage";
import MyDownloadsPage from "./pages/MyDownloadsPage"; import MyAppsPage from "./pages/MyAppsPage";
const BASE_URL = import.meta.env.BASE_URL; const BASE_URL = import.meta.env.BASE_URL;
@ -22,7 +22,7 @@ function App() {
<Header /> <Header />
<Routes> <Routes>
<Route path={STORE_PATH} element={<StorePage />} /> <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={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />
<Route path={PUBLISH_PATH} element={<PublishPage />} /> <Route path={PUBLISH_PATH} element={<PublishPage />} />
<Route path={`${DOWNLOAD_PATH}/:id`} element={<DownloadPage />} /> <Route path={`${DOWNLOAD_PATH}/:id`} element={<DownloadPage />} />

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; 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 { ConnectButton } from '@rainbow-me/rainbowkit';
import { FaHome } from "react-icons/fa"; import { FaHome } from "react-icons/fa";
@ -9,12 +9,12 @@ const Header: React.FC = () => {
<header className="app-header"> <header className="app-header">
<div className="header-left"> <div className="header-left">
<nav> <nav>
<button onClick={() => window.location.href = '/'}> <button onClick={() => window.location.href = '/'} className="home-button">
<FaHome /> <FaHome />
</button> </button>
<Link to={STORE_PATH} className={location.pathname === STORE_PATH ? 'active' : ''}>Apps</Link> <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={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> </nav>
</div> </div>
<div className="header-right"> <div className="header-right">

View File

@ -2,4 +2,4 @@ export const STORE_PATH = '/';
export const PUBLISH_PATH = '/publish'; export const PUBLISH_PATH = '/publish';
export const APP_DETAILS_PATH = '/app'; export const APP_DETAILS_PATH = '/app';
export const DOWNLOAD_PATH = '/download'; export const DOWNLOAD_PATH = '/download';
export const MY_DOWNLOADS_PATH = '/my-downloads'; export const MY_APPS_PATH = '/my-apps';

View File

@ -136,6 +136,8 @@ td {
.app-icon { .app-icon {
width: 64px; width: 64px;
height: 64px; height: 64px;
min-width: 64px;
min-height: 64px;
object-fit: cover; object-fit: cover;
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }
@ -348,6 +350,13 @@ td {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.home-button {
min-width: 48px;
min-height: 48px;
width: 48px;
height: 48px;
}
.app-screenshot { .app-screenshot {
max-width: 200px; max-width: 200px;
height: auto; height: auto;

View File

@ -3,15 +3,41 @@ import { FaFolder, FaFile, FaChevronLeft, FaSync, FaRocket, FaSpinner, FaCheck,
import useAppsStore from "../store"; import useAppsStore from "../store";
import { DownloadItem, PackageManifest, PackageState } from "../types/Apps"; import { DownloadItem, PackageManifest, PackageState } from "../types/Apps";
export default function MyDownloadsPage() { // Core packages that cannot be uninstalled
const { fetchDownloads, fetchDownloadsForApp, startMirroring, stopMirroring, installApp, removeDownload, fetchInstalled, installed } = useAppsStore(); 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 [currentPath, setCurrentPath] = useState<string[]>([]);
const [items, setItems] = useState<DownloadItem[]>([]); const [items, setItems] = useState<DownloadItem[]>([]);
const [isInstalling, setIsInstalling] = useState(false); const [isInstalling, setIsInstalling] = useState(false);
const [isUninstalling, setIsUninstalling] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showCapApproval, setShowCapApproval] = useState(false); const [showCapApproval, setShowCapApproval] = useState(false);
const [manifest, setManifest] = useState<PackageManifest | null>(null); const [manifest, setManifest] = useState<PackageManifest | null>(null);
const [selectedItem, setSelectedItem] = useState<DownloadItem | null>(null); const [selectedItem, setSelectedItem] = useState<DownloadItem | null>(null);
const [showUninstallConfirm, setShowUninstallConfirm] = useState(false);
const [appToUninstall, setAppToUninstall] = useState<any>(null);
useEffect(() => { useEffect(() => {
loadItems(); 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) => { const navigateToItem = (item: DownloadItem) => {
if (item.Dir) { if (item.Dir) {
setCurrentPath([...currentPath, item.Dir.name]); setCurrentPath([...currentPath, item.Dir.name]);
@ -85,7 +140,6 @@ export default function MyDownloadsPage() {
if (!versionHash) throw new Error('Invalid file name format'); 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(':'); const packageId = [...currentPath, ...parts].join(':');
await installApp(packageId, versionHash); await installApp(packageId, versionHash);
@ -121,8 +175,48 @@ export default function MyDownloadsPage() {
return ( return (
<div className="downloads-page"> <div className="downloads-page">
<h2>Downloads</h2> <h2>My Apps</h2>
{/* Installed Apps Section */}
<div className="file-explorer"> <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"> <div className="path-navigation">
{currentPath.length > 0 && ( {currentPath.length > 0 && (
<button onClick={navigateUp} className="navigate-up"> <button onClick={navigateUp} className="navigate-up">
@ -172,7 +266,8 @@ export default function MyDownloadsPage() {
)} )}
{isFile && isInstalled && ( {isFile && isInstalled && (
<FaCheck className="installed" /> <FaCheck className="installed" />
)} </td> )}
</td>
</tr> </tr>
); );
})} })}
@ -186,6 +281,45 @@ export default function MyDownloadsPage() {
</div> </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 && ( {showCapApproval && manifest && (
<div className="cap-approval-popup"> <div className="cap-approval-popup">
<div className="cap-approval-content"> <div className="cap-approval-content">

View File

@ -17,7 +17,7 @@ The format is "/" + "process_name:package_name:publisher_node"
const BASE_URL = `/main:app_store:sys`; const BASE_URL = `/main:app_store:sys`;
// This is the proxy URL, it must match the node you are developing against // 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); console.log('process.env.VITE_NODE_URL', process.env.VITE_NODE_URL, PROXY_URL);