mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 08:32:23 +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 { 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 />} />
|
||||||
|
@ -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">
|
||||||
|
@ -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';
|
||||||
|
@ -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;
|
||||||
|
@ -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">
|
@ -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);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user