fix(UI): unifies Supply Depot icon and improves loading UX (#1022)
This commit is contained in:
parent
5181637926
commit
02c9f72bf0
|
|
@ -8,7 +8,7 @@ interface LoadingSpinnerProps {
|
|||
|
||||
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
||||
text,
|
||||
fullscreen = true,
|
||||
fullscreen = false,
|
||||
iconOnly = false,
|
||||
light = false,
|
||||
className,
|
||||
|
|
@ -29,9 +29,12 @@ const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="ui active inverted dimmer">
|
||||
<div className="ui text loader">{!iconOnly && <span>{text || 'Loading'}</span>}</div>
|
||||
<div
|
||||
className={`fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm ${className || ''}`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="w-10 h-10 border-[3px] border-white border-t-transparent rounded-full animate-spin" />
|
||||
{!iconOnly && <div className="text-white mt-3 font-medium">{text || 'Loading'}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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: <IconPlus size={48} />,
|
||||
icon: <IconBox size={48} />,
|
||||
installed: true,
|
||||
displayOrder: 51,
|
||||
poweredBy: null,
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<AppLayout>
|
||||
<Head title="Supply Depot" />
|
||||
|
||||
{loading && <LoadingSpinner fullscreen text="Working..." />}
|
||||
{loading && !modal && <LoadingSpinner fullscreen text="Working..." />}
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* ── Hero / controls panel ─────────────────────────────────────────── */}
|
||||
|
|
@ -532,7 +541,10 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim
|
|||
<StyledModal
|
||||
title={`Install ${modal.service.friendly_name ?? modal.service.service_name}`}
|
||||
open
|
||||
onCancel={() => 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
|
|||
<StyledModal
|
||||
title={`Start ${modal.service.friendly_name ?? modal.service.service_name}`}
|
||||
open
|
||||
onCancel={() => 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
|
|||
<StyledModal
|
||||
title={`Stop ${modal.service.friendly_name ?? modal.service.service_name}`}
|
||||
open
|
||||
onCancel={() => 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
|
|||
<StyledModal
|
||||
title={`Restart ${modal.service.friendly_name ?? modal.service.service_name}`}
|
||||
open
|
||||
onCancel={() => 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
|
|||
<StyledModal
|
||||
title={`Force Reinstall ${modal.service.friendly_name ?? modal.service.service_name}`}
|
||||
open
|
||||
onCancel={() => 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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue