mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-11-22 19:34:06 +03:00
app_store UI: add wip notification bay
This commit is contained in:
parent
6eb02b0670
commit
5e77a5807d
@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { STORE_PATH, PUBLISH_PATH, MY_APPS_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";
|
||||||
|
import NotificationBay from './NotificationBay';
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@ -18,10 +19,10 @@ const Header: React.FC = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-right">
|
<div className="header-right">
|
||||||
|
<NotificationBay />
|
||||||
<ConnectButton />
|
<ConnectButton />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ManifestResponse, PackageManifestEntry } from '../types/Apps';
|
||||||
|
|
||||||
|
interface ManifestDisplayProps {
|
||||||
|
manifestResponse: ManifestResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const capabilityMap: Record<string, string> = {
|
||||||
|
'vfs:distro:sys': 'Virtual Filesystem',
|
||||||
|
'http_client:distro:sys': 'HTTP Client',
|
||||||
|
'http_server:distro:sys': 'HTTP Server',
|
||||||
|
'eth:distro:sys': 'Ethereum RPC access',
|
||||||
|
'homepage:homepage:sys': 'Ability to add itself to homepage',
|
||||||
|
'main:app_store:sys': 'App Store',
|
||||||
|
'chain:app_store:sys': 'Chain',
|
||||||
|
'terminal:terminal:sys': 'Terminal',
|
||||||
|
};
|
||||||
|
|
||||||
|
// note: we can do some future regex magic mapping here too!
|
||||||
|
// if includes("root") return WARNING
|
||||||
|
const transformCapabilities = (capabilities: any[]) => {
|
||||||
|
return capabilities.map(cap => capabilityMap[cap] || cap);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const renderManifest = (manifest: PackageManifestEntry) => (
|
||||||
|
<div className="manifest-item">
|
||||||
|
<p><strong>Process Name:</strong> {manifest.process_name}</p>
|
||||||
|
<p><strong>Requests Network Access:</strong> {manifest.request_networking ? 'Yes' : 'No'}</p>
|
||||||
|
<p><strong>Wants to Grant Capabilities:</strong> {transformCapabilities(manifest.grant_capabilities).join(', ') || 'None'}</p>
|
||||||
|
<p><strong>Is Public:</strong> {manifest.public ? 'Yes' : 'No'}</p>
|
||||||
|
<p><strong>Requests the Capabilities:</strong> {transformCapabilities(manifest.request_capabilities).join(', ') || 'None'}</p>
|
||||||
|
{/* <p><strong>Process Wasm Path:</strong> {manifest.process_wasm_path}</p> */}
|
||||||
|
{/* <p><strong>On Exit:</strong> {manifest.on_exit}</p> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ManifestDisplay: React.FC<ManifestDisplayProps> = ({ manifestResponse }) => {
|
||||||
|
if (!manifestResponse) {
|
||||||
|
return <p>No manifest data available.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedManifests: PackageManifestEntry[] = JSON.parse(manifestResponse.manifest);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="manifest-display">
|
||||||
|
{parsedManifests.map((manifest, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
{renderManifest(manifest)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManifestDisplay;
|
135
kinode/packages/app_store/ui/src/components/NotificationBay.tsx
Normal file
135
kinode/packages/app_store/ui/src/components/NotificationBay.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React, { ReactNode, useState } from 'react';
|
||||||
|
import { FaBell, FaChevronDown, FaChevronUp, FaTrash, FaTimes } from 'react-icons/fa';
|
||||||
|
import useAppsStore from '../store';
|
||||||
|
import { Notification, NotificationAction } from '../types/Apps';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
children: ReactNode;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal: React.FC<ModalProps> = ({ children, onClose }) => {
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal-content">
|
||||||
|
<button className="modal-close" onClick={onClose}>
|
||||||
|
<FaTimes />
|
||||||
|
</button>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotificationBay: React.FC = () => {
|
||||||
|
const { notifications, removeNotification } = useAppsStore();
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [modalContent, setModalContent] = useState<React.ReactNode | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleActionClick = (action: NotificationAction) => {
|
||||||
|
switch (action.action.type) {
|
||||||
|
case 'modal':
|
||||||
|
const content = typeof action.action.modalContent === 'function'
|
||||||
|
? action.action.modalContent()
|
||||||
|
: action.action.modalContent;
|
||||||
|
setModalContent(content);
|
||||||
|
break;
|
||||||
|
case 'click':
|
||||||
|
action.action.onClick?.();
|
||||||
|
break;
|
||||||
|
case 'redirect':
|
||||||
|
if (action.action.path) {
|
||||||
|
navigate(action.action.path);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDismiss = (notificationId: string, event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation(); // Prevent event bubbling
|
||||||
|
removeNotification(notificationId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNotification = (notification: Notification) => {
|
||||||
|
return (
|
||||||
|
<div key={notification.id} className={`notification-item ${notification.type}`}>
|
||||||
|
{notification.renderContent ? (
|
||||||
|
notification.renderContent(notification)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="notification-content">
|
||||||
|
<p>{notification.message}</p>
|
||||||
|
{notification.type === 'download' && notification.metadata?.progress && (
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div
|
||||||
|
className="progress"
|
||||||
|
style={{ width: `${notification.metadata.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{notification.actions && (
|
||||||
|
<div className="notification-actions">
|
||||||
|
{notification.actions.map((action, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => handleActionClick(action)}
|
||||||
|
className={`action-button ${action.variant || 'secondary'}`}
|
||||||
|
>
|
||||||
|
{action.icon && <action.icon />}
|
||||||
|
{action.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!notification.persistent && (
|
||||||
|
<button
|
||||||
|
className="dismiss-button"
|
||||||
|
onClick={(e) => handleDismiss(notification.id, e)}
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="notification-bay">
|
||||||
|
<button onClick={() => setIsExpanded(!isExpanded)} className="notification-button">
|
||||||
|
<FaBell />
|
||||||
|
{notifications.length > 0 && (
|
||||||
|
<span className="badge">{notifications.length}</span>
|
||||||
|
)}
|
||||||
|
{isExpanded ? <FaChevronUp /> : <FaChevronDown />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="notification-details">
|
||||||
|
{notifications.length === 0 ? (
|
||||||
|
<p>All clear, no notifications!</p>
|
||||||
|
) : (
|
||||||
|
notifications.map(renderNotification)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{modalContent && (
|
||||||
|
<Modal onClose={() => setModalContent(null)}>
|
||||||
|
{modalContent}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationBay;
|
@ -1,3 +1,5 @@
|
|||||||
export { default as Header } from './Header';
|
export { default as Header } from './Header';
|
||||||
export { default as MirrorSelector } from './MirrorSelector';
|
export { default as MirrorSelector } from './MirrorSelector';
|
||||||
export { default as PackageSelector } from './PackageSelector';
|
export { default as PackageSelector } from './PackageSelector';
|
||||||
|
export { default as ManifestDisplay } from './ManifestDisplay';
|
||||||
|
export { default as NotificationBay } from './NotificationBay';
|
@ -49,6 +49,13 @@ a:hover {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
/* Provides consistent spacing between NotificationBay and ConnectButton */
|
||||||
|
}
|
||||||
|
|
||||||
.header-left h1 {
|
.header-left h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@ -432,4 +439,167 @@ td {
|
|||||||
|
|
||||||
.fa-spin {
|
.fa-spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-display {
|
||||||
|
background-color: light-dark(var(--tan), var(--tasteful-dark));
|
||||||
|
color: light-dark(var(--off-black), var(--off-white));
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-item {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--gray);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: light-dark(var(--white), var(--off-black));
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-item p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bay {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: light-dark(var(--off-black), var(--off-white));
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-details {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
width: 320px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: light-dark(var(--white), var(--tasteful-dark));
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background-color: var(--orange);
|
||||||
|
color: var(--white);
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: light-dark(var(--tan), var(--off-black));
|
||||||
|
color: light-dark(var(--off-black), var(--off-white));
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.error {
|
||||||
|
background-color: light-dark(#ffe6e6, #4a2020);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.success {
|
||||||
|
background-color: light-dark(#e6ffe6, #204a20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.warning {
|
||||||
|
background-color: light-dark(#fff3e6, #4a3820);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.download {
|
||||||
|
background-color: light-dark(#e6f3ff, #20304a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dismiss-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: light-dark(var(--gray), var(--off-white));
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dismiss-button:hover {
|
||||||
|
color: var(--orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
height: 4px;
|
||||||
|
background-color: light-dark(var(--white), var(--off-black));
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--orange);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal styles */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: light-dark(var(--white), var(--tasteful-dark));
|
||||||
|
color: light-dark(var(--off-black), var(--off-white));
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
position: relative;
|
||||||
|
max-width: 80%;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: light-dark(var(--gray), var(--off-white));
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
color: var(--orange);
|
||||||
}
|
}
|
@ -2,7 +2,8 @@ import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { FaDownload, FaSpinner, FaChevronDown, FaChevronUp, FaRocket, FaTrash, FaPlay } from "react-icons/fa";
|
import { FaDownload, FaSpinner, FaChevronDown, FaChevronUp, FaRocket, FaTrash, FaPlay } from "react-icons/fa";
|
||||||
import useAppsStore from "../store";
|
import useAppsStore from "../store";
|
||||||
import { MirrorSelector } from '../components';
|
import { MirrorSelector, ManifestDisplay } from '../components';
|
||||||
|
import { ManifestResponse } from "../types/Apps";
|
||||||
|
|
||||||
export default function DownloadPage() {
|
export default function DownloadPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@ -17,7 +18,7 @@ export default function DownloadPage() {
|
|||||||
removeDownload,
|
removeDownload,
|
||||||
clearAllActiveDownloads,
|
clearAllActiveDownloads,
|
||||||
fetchHomepageApps,
|
fetchHomepageApps,
|
||||||
getLaunchUrl
|
getLaunchUrl,
|
||||||
} = useAppsStore();
|
} = useAppsStore();
|
||||||
|
|
||||||
const [showMetadata, setShowMetadata] = useState(false);
|
const [showMetadata, setShowMetadata] = useState(false);
|
||||||
@ -26,7 +27,7 @@ export default function DownloadPage() {
|
|||||||
const [showMyDownloads, setShowMyDownloads] = useState(false);
|
const [showMyDownloads, setShowMyDownloads] = useState(false);
|
||||||
const [isMirrorOnline, setIsMirrorOnline] = useState<boolean | null>(null);
|
const [isMirrorOnline, setIsMirrorOnline] = useState<boolean | null>(null);
|
||||||
const [showCapApproval, setShowCapApproval] = useState(false);
|
const [showCapApproval, setShowCapApproval] = useState(false);
|
||||||
const [manifest, setManifest] = useState<any>(null);
|
const [manifestResponse, setManifestResponse] = useState<ManifestResponse | null>(null);
|
||||||
const [isInstalling, setIsInstalling] = useState(false);
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
const [isCheckingLaunch, setIsCheckingLaunch] = useState(false);
|
const [isCheckingLaunch, setIsCheckingLaunch] = useState(false);
|
||||||
const [launchPath, setLaunchPath] = useState<string | null>(null);
|
const [launchPath, setLaunchPath] = useState<string | null>(null);
|
||||||
@ -151,8 +152,12 @@ export default function DownloadPage() {
|
|||||||
const download = appDownloads.find(d => d.File && d.File.name === `${hash}.zip`);
|
const download = appDownloads.find(d => d.File && d.File.name === `${hash}.zip`);
|
||||||
if (download?.File?.manifest) {
|
if (download?.File?.manifest) {
|
||||||
try {
|
try {
|
||||||
const manifestData = JSON.parse(download.File.manifest);
|
const manifest_response: ManifestResponse = {
|
||||||
setManifest(manifestData);
|
package_id: app.package_id,
|
||||||
|
version_hash: hash,
|
||||||
|
manifest: download.File.manifest
|
||||||
|
};
|
||||||
|
setManifestResponse(manifest_response);
|
||||||
setShowCapApproval(true);
|
setShowCapApproval(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse manifest:', error);
|
console.error('Failed to parse manifest:', error);
|
||||||
@ -170,7 +175,7 @@ export default function DownloadPage() {
|
|||||||
setLaunchPath(null);
|
setLaunchPath(null);
|
||||||
installApp(id, versionData.hash).then(() => {
|
installApp(id, versionData.hash).then(() => {
|
||||||
setShowCapApproval(false);
|
setShowCapApproval(false);
|
||||||
setManifest(null);
|
setManifestResponse(null);
|
||||||
fetchData(id);
|
fetchData(id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -337,13 +342,11 @@ export default function DownloadPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCapApproval && manifest && (
|
{showCapApproval && manifestResponse && (
|
||||||
<div className="cap-approval-popup">
|
<div className="cap-approval-popup">
|
||||||
<div className="cap-approval-content">
|
<div className="cap-approval-content">
|
||||||
<h3>Approve Capabilities</h3>
|
<h3>Approve Capabilities</h3>
|
||||||
<pre className="json-display">
|
<ManifestDisplay manifestResponse={manifestResponse} />
|
||||||
{JSON.stringify(manifest[0]?.request_capabilities || [], null, 2)}
|
|
||||||
</pre>
|
|
||||||
<div className="approval-buttons">
|
<div className="approval-buttons">
|
||||||
<button onClick={() => setShowCapApproval(false)}>Cancel</button>
|
<button onClick={() => setShowCapApproval(false)}>Cancel</button>
|
||||||
<button onClick={confirmInstall}>
|
<button onClick={confirmInstall}>
|
||||||
@ -354,7 +357,6 @@ export default function DownloadPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<div className="app-details">
|
<div className="app-details">
|
||||||
<h3>App Details</h3>
|
<h3>App Details</h3>
|
||||||
<button onClick={() => setShowMetadata(!showMetadata)}>
|
<button onClick={() => setShowMetadata(!showMetadata)}>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { persist } from 'zustand/middleware'
|
import { persist } from 'zustand/middleware'
|
||||||
import { PackageState, AppListing, MirrorCheckFile, PackageManifest, DownloadItem, HomepageApp } from '../types/Apps'
|
import { PackageState, AppListing, MirrorCheckFile, DownloadItem, HomepageApp, ManifestResponse, Notification } from '../types/Apps'
|
||||||
import { HTTP_STATUS } from '../constants/http'
|
import { HTTP_STATUS } from '../constants/http'
|
||||||
import KinodeClientApi from "@kinode/client-api"
|
import KinodeClientApi from "@kinode/client-api"
|
||||||
import { WEBSOCKET_URL } from '../utils/ws'
|
import { WEBSOCKET_URL } from '../utils/ws'
|
||||||
@ -13,6 +13,7 @@ interface AppsStore {
|
|||||||
downloads: Record<string, DownloadItem[]>
|
downloads: Record<string, DownloadItem[]>
|
||||||
ourApps: AppListing[]
|
ourApps: AppListing[]
|
||||||
ws: KinodeClientApi
|
ws: KinodeClientApi
|
||||||
|
notifications: Notification[]
|
||||||
homepageApps: HomepageApp[]
|
homepageApps: HomepageApp[]
|
||||||
activeDownloads: Record<string, { downloaded: number, total: number }>
|
activeDownloads: Record<string, { downloaded: number, total: number }>
|
||||||
|
|
||||||
@ -29,11 +30,15 @@ interface AppsStore {
|
|||||||
fetchHomepageApps: () => Promise<void>
|
fetchHomepageApps: () => Promise<void>
|
||||||
getLaunchUrl: (id: string) => string | null
|
getLaunchUrl: (id: string) => string | null
|
||||||
|
|
||||||
|
addNotification: (notification: Notification) => void;
|
||||||
|
removeNotification: (id: string) => void;
|
||||||
|
clearNotifications: () => void;
|
||||||
|
|
||||||
installApp: (id: string, version_hash: string) => Promise<void>
|
installApp: (id: string, version_hash: string) => Promise<void>
|
||||||
uninstallApp: (id: string) => Promise<void>
|
uninstallApp: (id: string) => Promise<void>
|
||||||
downloadApp: (id: string, version_hash: string, downloadFrom: string) => Promise<void>
|
downloadApp: (id: string, version_hash: string, downloadFrom: string) => Promise<void>
|
||||||
removeDownload: (packageId: string, versionHash: string) => Promise<void>
|
removeDownload: (packageId: string, versionHash: string) => Promise<void>
|
||||||
getCaps: (id: string) => Promise<PackageManifest | null>
|
getManifest: (id: string, version_hash: string) => Promise<ManifestResponse | null>
|
||||||
approveCaps: (id: string) => Promise<void>
|
approveCaps: (id: string) => Promise<void>
|
||||||
startMirroring: (id: string) => Promise<void>
|
startMirroring: (id: string) => Promise<void>
|
||||||
stopMirroring: (id: string) => Promise<void>
|
stopMirroring: (id: string) => Promise<void>
|
||||||
@ -52,6 +57,7 @@ const useAppsStore = create<AppsStore>()((set, get) => ({
|
|||||||
ourApps: [],
|
ourApps: [],
|
||||||
activeDownloads: {},
|
activeDownloads: {},
|
||||||
homepageApps: [],
|
homepageApps: [],
|
||||||
|
notifications: [],
|
||||||
|
|
||||||
|
|
||||||
fetchData: async (id: string) => {
|
fetchData: async (id: string) => {
|
||||||
@ -282,14 +288,14 @@ const useAppsStore = create<AppsStore>()((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getCaps: async (id: string) => {
|
getManifest: async (id: string, version_hash: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BASE_URL}/apps/${id}/caps`);
|
const res = await fetch(`${BASE_URL}/manifest?id=${id}&version_hash=${version_hash}`);
|
||||||
if (res.status === HTTP_STATUS.OK) {
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
return await res.json() as PackageManifest;
|
return await res.json() as ManifestResponse;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting caps:", error);
|
console.error("Error getting manifest:", error);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@ -355,6 +361,18 @@ const useAppsStore = create<AppsStore>()((set, get) => ({
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
addNotification: (notification) => set(state => ({
|
||||||
|
notifications: [...state.notifications, notification]
|
||||||
|
})),
|
||||||
|
|
||||||
|
removeNotification: (id) => set(state => ({
|
||||||
|
notifications: state.notifications.filter(n => n.id !== id)
|
||||||
|
})),
|
||||||
|
|
||||||
|
clearNotifications: () => set({ notifications: [] }),
|
||||||
|
|
||||||
|
|
||||||
clearActiveDownload: (appId) => {
|
clearActiveDownload: (appId) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const { [appId]: _, ...rest } = state.activeDownloads;
|
const { [appId]: _, ...rest } = state.activeDownloads;
|
||||||
@ -374,10 +392,40 @@ const useAppsStore = create<AppsStore>()((set, get) => ({
|
|||||||
const { package_id, version_hash, downloaded, total } = data.data;
|
const { package_id, version_hash, downloaded, total } = data.data;
|
||||||
const appId = `${package_id.package_name}:${package_id.publisher_node}:${version_hash}`;
|
const appId = `${package_id.package_name}:${package_id.publisher_node}:${version_hash}`;
|
||||||
get().setActiveDownload(appId, downloaded, total);
|
get().setActiveDownload(appId, downloaded, total);
|
||||||
|
|
||||||
|
const existingNotification = get().notifications.find(
|
||||||
|
n => n.id === `download-${appId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingNotification) {
|
||||||
|
get().removeNotification(`download-${appId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get().addNotification({
|
||||||
|
id: `download-${appId}`,
|
||||||
|
type: 'download',
|
||||||
|
message: `Downloading ${package_id.package_name}`,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
metadata: {
|
||||||
|
packageId: `${package_id.package_name}:${package_id.publisher_node}`,
|
||||||
|
versionHash: version_hash,
|
||||||
|
progress: Math.round((downloaded / total) * 100)
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (data.kind === 'complete') {
|
} else if (data.kind === 'complete') {
|
||||||
const { package_id, version_hash } = data.data;
|
const { package_id, version_hash } = data.data;
|
||||||
const appId = `${package_id.package_name}:${package_id.publisher_node}:${version_hash}`;
|
const appId = `${package_id.package_name}:${package_id.publisher_node}:${version_hash}`;
|
||||||
get().clearActiveDownload(appId);
|
get().clearActiveDownload(appId);
|
||||||
|
|
||||||
|
get().removeNotification(`download-${appId}`);
|
||||||
|
|
||||||
|
get().addNotification({
|
||||||
|
id: `complete-${appId}`,
|
||||||
|
type: 'success',
|
||||||
|
message: `Download complete: ${package_id.package_name}`,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
get().fetchData(`${package_id.package_name}:${package_id.publisher_node}`);
|
get().fetchData(`${package_id.package_name}:${package_id.publisher_node}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { IconType } from "react-icons/lib";
|
||||||
|
|
||||||
export interface PackageId {
|
export interface PackageId {
|
||||||
package_name: string;
|
package_name: string;
|
||||||
publisher_node: string;
|
publisher_node: string;
|
||||||
@ -59,9 +62,10 @@ export interface PackageState {
|
|||||||
our_version_hash: string;
|
our_version_hash: string;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
caps_approved: boolean;
|
caps_approved: boolean;
|
||||||
|
pending_update_hash?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PackageManifest {
|
export interface PackageManifestEntry {
|
||||||
process_name: string
|
process_name: string
|
||||||
process_wasm_path: string
|
process_wasm_path: string
|
||||||
on_exit: string
|
on_exit: string
|
||||||
@ -71,6 +75,12 @@ export interface PackageManifest {
|
|||||||
public: boolean
|
public: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ManifestResponse {
|
||||||
|
package_id: PackageId;
|
||||||
|
version_hash: string;
|
||||||
|
manifest: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HomepageApp {
|
export interface HomepageApp {
|
||||||
id: string;
|
id: string;
|
||||||
process: string;
|
process: string;
|
||||||
@ -83,3 +93,35 @@ export interface HomepageApp {
|
|||||||
order: number;
|
order: number;
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type NotificationActionType = 'click' | 'modal' | 'popup' | 'redirect';
|
||||||
|
|
||||||
|
export type NotificationAction = {
|
||||||
|
label: string;
|
||||||
|
icon?: IconType;
|
||||||
|
variant?: 'primary' | 'secondary' | 'danger';
|
||||||
|
action: {
|
||||||
|
type: NotificationActionType;
|
||||||
|
onClick?: () => void;
|
||||||
|
modalContent?: ReactNode | (() => ReactNode);
|
||||||
|
popupContent?: ReactNode | (() => ReactNode);
|
||||||
|
path?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Notification = {
|
||||||
|
id: string;
|
||||||
|
type: 'error' | 'success' | 'warning' | 'info' | 'download' | 'install' | 'update';
|
||||||
|
message: string;
|
||||||
|
timestamp: number;
|
||||||
|
metadata?: {
|
||||||
|
packageId?: string;
|
||||||
|
versionHash?: string;
|
||||||
|
progress?: number;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
actions?: NotificationAction[];
|
||||||
|
renderContent?: (notification: Notification) => ReactNode;
|
||||||
|
persistent?: boolean;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user