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 { ConnectButton } from '@rainbow-me/rainbowkit';
|
||||
import { FaHome } from "react-icons/fa";
|
||||
import NotificationBay from './NotificationBay';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
return (
|
||||
@ -18,10 +19,10 @@ const Header: React.FC = () => {
|
||||
</nav>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<NotificationBay />
|
||||
<ConnectButton />
|
||||
</div>
|
||||
</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 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;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
/* Provides consistent spacing between NotificationBay and ConnectButton */
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
@ -432,4 +439,167 @@ td {
|
||||
|
||||
.fa-spin {
|
||||
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 { FaDownload, FaSpinner, FaChevronDown, FaChevronUp, FaRocket, FaTrash, FaPlay } from "react-icons/fa";
|
||||
import useAppsStore from "../store";
|
||||
import { MirrorSelector } from '../components';
|
||||
import { MirrorSelector, ManifestDisplay } from '../components';
|
||||
import { ManifestResponse } from "../types/Apps";
|
||||
|
||||
export default function DownloadPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@ -17,7 +18,7 @@ export default function DownloadPage() {
|
||||
removeDownload,
|
||||
clearAllActiveDownloads,
|
||||
fetchHomepageApps,
|
||||
getLaunchUrl
|
||||
getLaunchUrl,
|
||||
} = useAppsStore();
|
||||
|
||||
const [showMetadata, setShowMetadata] = useState(false);
|
||||
@ -26,7 +27,7 @@ export default function DownloadPage() {
|
||||
const [showMyDownloads, setShowMyDownloads] = useState(false);
|
||||
const [isMirrorOnline, setIsMirrorOnline] = useState<boolean | null>(null);
|
||||
const [showCapApproval, setShowCapApproval] = useState(false);
|
||||
const [manifest, setManifest] = useState<any>(null);
|
||||
const [manifestResponse, setManifestResponse] = useState<ManifestResponse | null>(null);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [isCheckingLaunch, setIsCheckingLaunch] = useState(false);
|
||||
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`);
|
||||
if (download?.File?.manifest) {
|
||||
try {
|
||||
const manifestData = JSON.parse(download.File.manifest);
|
||||
setManifest(manifestData);
|
||||
const manifest_response: ManifestResponse = {
|
||||
package_id: app.package_id,
|
||||
version_hash: hash,
|
||||
manifest: download.File.manifest
|
||||
};
|
||||
setManifestResponse(manifest_response);
|
||||
setShowCapApproval(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse manifest:', error);
|
||||
@ -170,7 +175,7 @@ export default function DownloadPage() {
|
||||
setLaunchPath(null);
|
||||
installApp(id, versionData.hash).then(() => {
|
||||
setShowCapApproval(false);
|
||||
setManifest(null);
|
||||
setManifestResponse(null);
|
||||
fetchData(id);
|
||||
});
|
||||
}
|
||||
@ -337,13 +342,11 @@ export default function DownloadPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCapApproval && manifest && (
|
||||
{showCapApproval && manifestResponse && (
|
||||
<div className="cap-approval-popup">
|
||||
<div className="cap-approval-content">
|
||||
<h3>Approve Capabilities</h3>
|
||||
<pre className="json-display">
|
||||
{JSON.stringify(manifest[0]?.request_capabilities || [], null, 2)}
|
||||
</pre>
|
||||
<ManifestDisplay manifestResponse={manifestResponse} />
|
||||
<div className="approval-buttons">
|
||||
<button onClick={() => setShowCapApproval(false)}>Cancel</button>
|
||||
<button onClick={confirmInstall}>
|
||||
@ -354,7 +357,6 @@ export default function DownloadPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="app-details">
|
||||
<h3>App Details</h3>
|
||||
<button onClick={() => setShowMetadata(!showMetadata)}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
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 KinodeClientApi from "@kinode/client-api"
|
||||
import { WEBSOCKET_URL } from '../utils/ws'
|
||||
@ -13,6 +13,7 @@ interface AppsStore {
|
||||
downloads: Record<string, DownloadItem[]>
|
||||
ourApps: AppListing[]
|
||||
ws: KinodeClientApi
|
||||
notifications: Notification[]
|
||||
homepageApps: HomepageApp[]
|
||||
activeDownloads: Record<string, { downloaded: number, total: number }>
|
||||
|
||||
@ -29,11 +30,15 @@ interface AppsStore {
|
||||
fetchHomepageApps: () => Promise<void>
|
||||
getLaunchUrl: (id: string) => string | null
|
||||
|
||||
addNotification: (notification: Notification) => void;
|
||||
removeNotification: (id: string) => void;
|
||||
clearNotifications: () => void;
|
||||
|
||||
installApp: (id: string, version_hash: string) => Promise<void>
|
||||
uninstallApp: (id: string) => Promise<void>
|
||||
downloadApp: (id: string, version_hash: string, downloadFrom: 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>
|
||||
startMirroring: (id: string) => Promise<void>
|
||||
stopMirroring: (id: string) => Promise<void>
|
||||
@ -52,6 +57,7 @@ const useAppsStore = create<AppsStore>()((set, get) => ({
|
||||
ourApps: [],
|
||||
activeDownloads: {},
|
||||
homepageApps: [],
|
||||
notifications: [],
|
||||
|
||||
|
||||
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 {
|
||||
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) {
|
||||
return await res.json() as PackageManifest;
|
||||
return await res.json() as ManifestResponse;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting caps:", error);
|
||||
console.error("Error getting manifest:", error);
|
||||
}
|
||||
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) => {
|
||||
set((state) => {
|
||||
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 appId = `${package_id.package_name}:${package_id.publisher_node}:${version_hash}`;
|
||||
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') {
|
||||
const { package_id, version_hash } = data.data;
|
||||
const appId = `${package_id.package_name}:${package_id.publisher_node}:${version_hash}`;
|
||||
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}`);
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import { IconType } from "react-icons/lib";
|
||||
|
||||
export interface PackageId {
|
||||
package_name: string;
|
||||
publisher_node: string;
|
||||
@ -59,9 +62,10 @@ export interface PackageState {
|
||||
our_version_hash: string;
|
||||
verified: boolean;
|
||||
caps_approved: boolean;
|
||||
pending_update_hash?: string;
|
||||
}
|
||||
|
||||
export interface PackageManifest {
|
||||
export interface PackageManifestEntry {
|
||||
process_name: string
|
||||
process_wasm_path: string
|
||||
on_exit: string
|
||||
@ -71,6 +75,12 @@ export interface PackageManifest {
|
||||
public: boolean
|
||||
}
|
||||
|
||||
export interface ManifestResponse {
|
||||
package_id: PackageId;
|
||||
version_hash: string;
|
||||
manifest: string;
|
||||
}
|
||||
|
||||
export interface HomepageApp {
|
||||
id: string;
|
||||
process: string;
|
||||
@ -83,3 +93,35 @@ export interface HomepageApp {
|
||||
order: number;
|
||||
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