frontend: polish app launch and release experience
This commit is contained in:
@@ -12,7 +12,7 @@ vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
vi.mock('@/router', () => ({
|
||||
default: { push: mockPush },
|
||||
default: { push: mockPush, currentRoute: { value: { fullPath: '/dashboard/apps', name: 'apps' } } },
|
||||
}))
|
||||
|
||||
vi.stubGlobal('open', mockWindowOpen)
|
||||
@@ -66,7 +66,7 @@ describe('useAppLauncherStore', () => {
|
||||
store.openSession('indeedhub')
|
||||
|
||||
expect(store.panelAppId).toBe(null)
|
||||
expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'indeedhub' } })
|
||||
expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'indeedhub' }, query: { returnTo: '/dashboard/apps' } })
|
||||
})
|
||||
|
||||
it('normalizes localhost launch URLs to current host before resolving', () => {
|
||||
@@ -95,7 +95,12 @@ describe('useAppLauncherStore', () => {
|
||||
store.open({ url: 'http://192.168.1.228:23000', title: 'BTCPay' })
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(store.panelAppId).toBe('btcpay-server')
|
||||
expect(store.panelAppId).toBe(null)
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'http://192.168.1.228:23000',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
})
|
||||
|
||||
it('normalizes old Nginx Proxy Manager port 81 to 8081', () => {
|
||||
@@ -125,7 +130,7 @@ describe('useAppLauncherStore', () => {
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(store.panelAppId).toBe(null)
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'nginx-proxy-manager' } })
|
||||
expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'nginx-proxy-manager' }, query: { returnTo: '/dashboard/apps' } })
|
||||
})
|
||||
|
||||
it('opens Nginx Proxy Manager in new tab using title hint when URL is path-only', () => {
|
||||
@@ -186,7 +191,12 @@ describe('useAppLauncherStore', () => {
|
||||
store.open({ url: 'http://192.168.1.228:8123', title: 'Home Assistant' })
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(store.panelAppId).toBeTruthy()
|
||||
expect(store.panelAppId).toBe(null)
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'http://192.168.1.228:8123',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
})
|
||||
|
||||
it('routes Grafana (port 3000) to full-page session', () => {
|
||||
@@ -195,7 +205,12 @@ describe('useAppLauncherStore', () => {
|
||||
store.open({ url: 'http://192.168.1.228:3000', title: 'Grafana' })
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(store.panelAppId).toBeTruthy()
|
||||
expect(store.panelAppId).toBe(null)
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'http://192.168.1.228:3000',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
})
|
||||
|
||||
it('opens Gitea path URL in new tab', () => {
|
||||
@@ -261,7 +276,7 @@ describe('useAppLauncherStore', () => {
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'arch-presentation' } })
|
||||
expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'arch-presentation' }, query: { returnTo: '/dashboard/apps' } })
|
||||
})
|
||||
|
||||
it('routes HTTPS same-host apps via session view', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import router from '@/router'
|
||||
import { recordAppLaunch } from '@/utils/appUsage'
|
||||
|
||||
/** Ports of apps that set X-Frame-Options (can't iframe, must open in new tab) */
|
||||
const NEW_TAB_PORTS = new Set([
|
||||
@@ -102,8 +103,6 @@ const PORT_TO_APP_ID: Record<string, string> = {
|
||||
'8334': 'bitcoin-knots',
|
||||
'8888': 'searxng',
|
||||
'9000': 'portainer',
|
||||
'9010': 'saleor',
|
||||
'9011': 'saleor',
|
||||
'8087': 'netbird',
|
||||
'8086': 'netbird',
|
||||
'9980': 'onlyoffice',
|
||||
@@ -186,21 +185,24 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
|
||||
/** Open app in session view — panel mode uses store, overlay/fullscreen uses route */
|
||||
function dashboardReturnPath(): string {
|
||||
const current = router.currentRoute.value
|
||||
const current = router.currentRoute?.value
|
||||
if (!current) return '/dashboard/apps'
|
||||
const fullPath = current.fullPath || '/dashboard/apps'
|
||||
if (!fullPath.startsWith('/dashboard') || current.name === 'app-session') return '/dashboard/apps'
|
||||
return fullPath
|
||||
}
|
||||
|
||||
function openSession(appId: string) {
|
||||
recordAppLaunch(appId)
|
||||
const mobile = isMobileViewport()
|
||||
const launchUrl = NEW_TAB_APP_IDS.has(appId) ? directAppUrl(appId) : null
|
||||
if (launchUrl) {
|
||||
if (launchUrl && !mobile) {
|
||||
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
|
||||
const mode = localStorage.getItem(DISPLAY_MODE_KEY) || 'panel'
|
||||
if (mode === 'panel' && !isMobileViewport()) {
|
||||
if (mode === 'panel' && !mobile) {
|
||||
panelAppId.value = appId
|
||||
} else {
|
||||
panelAppId.value = null
|
||||
@@ -219,6 +221,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
const resolvedId = resolveAppIdFromUrl(launchUrl) || titleHintId
|
||||
|
||||
if (!isMobileViewport() && payload.openInNewTab) {
|
||||
if (resolvedId) recordAppLaunch(resolvedId)
|
||||
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
@@ -227,6 +230,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
// phones, route through the app session/webview so app icons behave like
|
||||
// native launchers and keep the user inside Archipelago.
|
||||
if (!isMobileViewport() && resolvedId && NEW_TAB_APP_IDS.has(resolvedId)) {
|
||||
recordAppLaunch(resolvedId)
|
||||
window.open(launchUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const PHASE_INFO: Record<InstallPhase, { progress: number; message: string; stat
|
||||
'pulling-image': { progress: 20, message: 'Downloading image…', status: 'downloading' },
|
||||
'creating-container': { progress: 70, message: 'Creating container…', status: 'installing' },
|
||||
'starting-container': { progress: 80, message: 'Starting container…', status: 'starting' },
|
||||
'waiting-healthy': { progress: 88, message: 'Waiting for container…', status: 'starting' },
|
||||
'waiting-healthy': { progress: 88, message: 'Finalizing first start…', status: 'starting' },
|
||||
'post-install': { progress: 95, message: 'Finalizing…', status: 'installing' },
|
||||
'done': { progress: 100, message: 'Installed', status: 'complete' },
|
||||
}
|
||||
@@ -126,6 +126,12 @@ export const useServerStore = defineStore('server', () => {
|
||||
installingApps.value.delete(appId)
|
||||
}
|
||||
}
|
||||
|
||||
if ((pkg.state as string) === 'removing') {
|
||||
uninstallingApps.value.add(appId)
|
||||
} else if (uninstallingApps.value.has(appId)) {
|
||||
uninstallingApps.value.delete(appId)
|
||||
}
|
||||
}
|
||||
// Clear installingApps entries for apps that vanished from backend data
|
||||
// Only clean up entries that have errored — active installs may take minutes to pull images
|
||||
@@ -183,8 +189,11 @@ export const useServerStore = defineStore('server', () => {
|
||||
return rpcClient.installPackage(id, marketplaceUrl, version)
|
||||
}
|
||||
|
||||
async function uninstallPackage(id: string): Promise<{ status: string; package_id: string }> {
|
||||
return rpcClient.uninstallPackage(id)
|
||||
async function uninstallPackage(
|
||||
id: string,
|
||||
options: { preserveData?: boolean } = {},
|
||||
): Promise<{ status: string; package_id: string }> {
|
||||
return rpcClient.uninstallPackage(id, options)
|
||||
}
|
||||
|
||||
async function startPackage(id: string): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user