From 02c9f72bf0b76a17d1488b26985e12d5b54eff1d Mon Sep 17 00:00:00 2001 From: Jake Turner <52841588+jakeaturner@users.noreply.github.com> Date: Fri, 19 Jun 2026 21:42:44 -0700 Subject: [PATCH] fix(UI): unifies Supply Depot icon and improves loading UX (#1022) --- admin/inertia/components/LoadingSpinner.tsx | 11 +-- admin/inertia/layouts/SettingsLayout.tsx | 4 +- admin/inertia/pages/home.tsx | 4 +- admin/inertia/pages/supply-depot.tsx | 79 ++++++++++++++------- 4 files changed, 65 insertions(+), 33 deletions(-) diff --git a/admin/inertia/components/LoadingSpinner.tsx b/admin/inertia/components/LoadingSpinner.tsx index 493e9b0..8744ac4 100644 --- a/admin/inertia/components/LoadingSpinner.tsx +++ b/admin/inertia/components/LoadingSpinner.tsx @@ -8,7 +8,7 @@ interface LoadingSpinnerProps { const LoadingSpinner: React.FC = ({ text, - fullscreen = true, + fullscreen = false, iconOnly = false, light = false, className, @@ -29,9 +29,12 @@ const LoadingSpinner: React.FC = ({ } return ( -
-
-
{!iconOnly && {text || 'Loading'}}
+
+
+
+ {!iconOnly &&
{text || 'Loading'}
}
) diff --git a/admin/inertia/layouts/SettingsLayout.tsx b/admin/inertia/layouts/SettingsLayout.tsx index 2f02be6..b014444 100644 --- a/admin/inertia/layouts/SettingsLayout.tsx +++ b/admin/inertia/layouts/SettingsLayout.tsx @@ -1,5 +1,6 @@ import { IconArrowBigUpLines, + IconBox, IconChartBar, IconDashboard, IconFolder, @@ -7,7 +8,6 @@ import { IconHeart, IconMapRoute, IconSettings, - IconTerminal2, IconWand, IconZoom } from '@tabler/icons-react' @@ -23,7 +23,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode const navigation = [ ...(aiAssistantInstallStatus.isInstalled ? [{ name: aiAssistantName, href: '/settings/models', icon: IconWand, current: false }] : []), - { name: 'Supply Depot', href: '/supply-depot', icon: IconTerminal2, current: false }, + { name: 'Supply Depot', href: '/supply-depot', icon: IconBox, current: false }, { name: 'Benchmark', href: '/settings/benchmark', icon: IconChartBar, current: false }, { name: 'Content Explorer', href: '/settings/zim/remote-explorer', icon: IconZoom, current: false }, { name: 'Content Manager', href: '/settings/zim', icon: IconFolder, current: false }, diff --git a/admin/inertia/pages/home.tsx b/admin/inertia/pages/home.tsx index bf38fc1..a782327 100644 --- a/admin/inertia/pages/home.tsx +++ b/admin/inertia/pages/home.tsx @@ -1,8 +1,8 @@ import { IconBolt, + IconBox, IconHelp, IconMapRoute, - IconPlus, IconSettings, IconWifiOff, } from '@tabler/icons-react' @@ -46,7 +46,7 @@ const SYSTEM_ITEMS = [ to: '/supply-depot', target: '', description: 'Browse and install curated apps, or add your own Docker container', - icon: , + icon: , installed: true, displayOrder: 51, poweredBy: null, diff --git a/admin/inertia/pages/supply-depot.tsx b/admin/inertia/pages/supply-depot.tsx index 3943e99..80a20ff 100644 --- a/admin/inertia/pages/supply-depot.tsx +++ b/admin/inertia/pages/supply-depot.tsx @@ -201,10 +201,6 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim (s) => s.service_name === SERVICE_NAMES.KOLIBRI_GEN2 && s.installed ) - useEffect(() => { - console.log("Education Gen 2 installed:", educationGen2Installed) - }, [educationGen2Installed]) - // ── Actions ─────────────────────────────────────────────────────────────── async function handleInstall(service: ServiceSlim) { const hasWarnings = @@ -212,48 +208,61 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim if (hasWarnings && !forceInstall) return + // Keep the modal open with a spinning confirm button while the request is in + // flight; close it once the install job is dispatched. Progress then streams + // via the InstallActivityFeed broadcast, so we drop the loading flag here. setLoading(true) - setModal(null) const result = await api.installService(service.service_name) + setModal(null) setLoading(false) if (!result?.success) showError(result?.message || 'Failed to start installation.') } async function handleAffect(service: ServiceSlim, action: 'start' | 'stop' | 'restart') { - setModal(null) setLoading(true) const result = await api.affectService(service.service_name, action) - setLoading(false) - if (!result?.success) showError(result?.message || `Failed to ${action} service.`) - else setTimeout(() => window.location.reload(), 1500) + setModal(null) + if (!result?.success) { + setLoading(false) + showError(result?.message || `Failed to ${action} service.`) + } else { + // Keep loading=true so the overlay covers the page until it reloads. + setTimeout(() => window.location.reload(), 1500) + } } async function handleForceReinstall(service: ServiceSlim) { - setModal(null) setLoading(true) const result = await api.forceReinstallService(service.service_name) + setModal(null) setLoading(false) if (!result?.success) showError(result?.message || 'Failed to start reinstall.') } async function handleDelete(service: ServiceSlim) { - setModal(null) setLoading(true) const result = await api.deleteCustomApp(service.service_name, removeImage) setRemoveImage(false) - setLoading(false) - if (!result?.success) showError(result?.message || 'Failed to delete app.') - else setTimeout(() => window.location.reload(), 1000) + setModal(null) + if (!result?.success) { + setLoading(false) + showError(result?.message || 'Failed to delete app.') + } else { + setTimeout(() => window.location.reload(), 1000) + } } async function handleUninstall(service: ServiceSlim) { - setModal(null) setLoading(true) const result = await api.uninstallService(service.service_name, removeImage) setRemoveImage(false) - setLoading(false) - if (!result?.success) showError(result?.message || 'Failed to uninstall app.') - else setTimeout(() => window.location.reload(), 1000) + setModal(null) + if (!result?.success) { + setLoading(false) + showError(result?.message || 'Failed to uninstall app.') + } else { + setTimeout(() => window.location.reload(), 1000) + } } async function handleUpdate(service: ServiceSlim) { @@ -351,7 +360,7 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim - {loading && } + {loading && !modal && }
{/* ── Hero / controls panel ─────────────────────────────────────────── */} @@ -532,7 +541,10 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim setModal(null)} + onCancel={() => { + if (loading) return + setModal(null) + }} onConfirm={() => handleInstall(modal.service)} confirmText="Install" confirmIcon="IconDownload" @@ -590,7 +602,10 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim setModal(null)} + onCancel={() => { + if (loading) return + setModal(null) + }} onConfirm={() => handleAffect(modal.service, 'start')} confirmText="Start" confirmIcon="IconPlayerPlay" @@ -606,7 +621,10 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim setModal(null)} + onCancel={() => { + if (loading) return + setModal(null) + }} onConfirm={() => handleAffect(modal.service, 'stop')} confirmText="Stop" confirmIcon="IconPlayerStop" @@ -622,7 +640,10 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim setModal(null)} + onCancel={() => { + if (loading) return + setModal(null) + }} onConfirm={() => handleAffect(modal.service, 'restart')} confirmText="Restart" confirmIcon="IconRefresh" @@ -638,7 +659,10 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim setModal(null)} + onCancel={() => { + if (loading) return + setModal(null) + }} onConfirm={() => handleForceReinstall(modal.service)} confirmText="Wipe & Reinstall" confirmIcon="IconRefresh" @@ -659,6 +683,7 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim title={`Delete ${modal.service.friendly_name ?? modal.service.service_name}`} open onCancel={() => { + if (loading) return setRemoveImage(false) setModal(null) }} @@ -691,6 +716,7 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim title={`Uninstall ${modal.service.friendly_name ?? modal.service.service_name}`} open onCancel={() => { + if (loading) return setRemoveImage(false) setModal(null) }} @@ -743,7 +769,10 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim record={modal.service} currentTag={extractTag(modal.service.container_image)} latestVersion={modal.service.available_update_version!} - onCancel={() => setModal(null)} + onCancel={() => { + if (loading) return + setModal(null) + }} onUpdate={(targetVersion) => { const service = modal.service setModal(null)