96 lines
4.1 KiB
Vue
96 lines
4.1 KiB
Vue
<template>
|
|
<div class="relative flex-1 min-h-0 bg-black/40 overflow-hidden app-session-frame-safe">
|
|
<Transition name="content-fade">
|
|
<div v-if="loading" class="absolute inset-0 z-10 flex items-center justify-center bg-black/40">
|
|
<svg class="animate-spin h-8 w-8 text-blue-400" viewBox="0 0 24 24" fill="none">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
</div>
|
|
</Transition>
|
|
|
|
<iframe
|
|
v-if="appUrl && !iframeBlocked"
|
|
ref="iframeRef"
|
|
:key="refreshKey"
|
|
:src="appUrl"
|
|
class="absolute inset-0 w-full h-full border-0 iframe-scrollbar-hide"
|
|
title="App content"
|
|
@load="$emit('iframeLoad')"
|
|
@error="$emit('iframeError')"
|
|
/>
|
|
|
|
<!-- Iframe blocked fallback -->
|
|
<Transition name="content-fade">
|
|
<div v-if="iframeBlocked" class="absolute inset-0 z-10 flex flex-col items-center justify-center">
|
|
<div class="text-center px-8">
|
|
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center">
|
|
<svg class="w-8 h-8 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-white mb-2">{{ mustOpenNewTab ? 'This app opens in a new tab' : 'App not reachable' }}</h3>
|
|
<p class="text-white/50 text-sm mb-6">
|
|
<template v-if="mustOpenNewTab">{{ appTitle }} sets security headers that prevent iframe embedding.<br>Open it in a new browser tab instead.</template>
|
|
<template v-else>{{ appTitle }} may still be starting up or the container is stopped.<br><span v-if="autoRetryCount > 0" class="text-yellow-400/70">Retrying automatically ({{ autoRetryCount }})...</span></template>
|
|
</p>
|
|
<div class="flex flex-wrap items-center justify-center gap-3">
|
|
<button
|
|
v-if="!mustOpenNewTab"
|
|
@click="$emit('refresh')"
|
|
class="glass-button px-6 py-3 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
|
|
>
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
Retry now
|
|
</button>
|
|
<button
|
|
@click="$emit('openNewTabAndBack')"
|
|
class="glass-button px-6 py-3 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
|
|
>
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
Open in new tab
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
|
|
<div v-if="!appUrl" class="absolute inset-0 flex items-center justify-center">
|
|
<div class="text-center px-8">
|
|
<h3 class="text-lg font-semibold text-white mb-2">App not configured</h3>
|
|
<p class="text-white/50 text-sm">No URL found for {{ appId }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
|
|
defineProps<{
|
|
appUrl: string
|
|
appId: string
|
|
appTitle: string
|
|
loading: boolean
|
|
iframeBlocked: boolean
|
|
mustOpenNewTab: boolean
|
|
autoRetryCount: number
|
|
refreshKey: number
|
|
}>()
|
|
|
|
defineEmits<{
|
|
iframeLoad: []
|
|
iframeError: []
|
|
refresh: []
|
|
openNewTabAndBack: []
|
|
}>()
|
|
|
|
const iframeRef = ref<HTMLIFrameElement | null>(null)
|
|
|
|
defineExpose({ iframeRef })
|
|
</script>
|