166 lines
5.6 KiB
Vue
166 lines
5.6 KiB
Vue
|
|
<template>
|
||
|
|
<div class="p-6">
|
||
|
|
<div class="mb-8">
|
||
|
|
<h1 class="text-3xl font-bold text-white mb-2">Container Apps</h1>
|
||
|
|
<p class="text-white/70">Manage containerized applications running on your Archipelago node</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Loading State -->
|
||
|
|
<div v-if="store.loading" class="flex items-center justify-center py-12">
|
||
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-white/60"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Error State -->
|
||
|
|
<div v-else-if="store.error" class="glass-card p-6 mb-6">
|
||
|
|
<div class="flex items-center gap-3 text-red-400">
|
||
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
|
|
</svg>
|
||
|
|
<span>{{ store.error }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Container List -->
|
||
|
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
|
|
<div
|
||
|
|
v-for="container in store.containers"
|
||
|
|
:key="container.id"
|
||
|
|
class="glass-card p-6 hover:bg-white/5 transition-colors cursor-pointer"
|
||
|
|
@click="$router.push(`/dashboard/containers/${extractAppId(container.name)}`)"
|
||
|
|
>
|
||
|
|
<div class="flex items-start justify-between mb-4">
|
||
|
|
<div class="flex-1">
|
||
|
|
<h3 class="text-lg font-semibold text-white mb-1">
|
||
|
|
{{ extractAppName(container.name) }}
|
||
|
|
</h3>
|
||
|
|
<p class="text-sm text-white/60">{{ container.image }}</p>
|
||
|
|
</div>
|
||
|
|
<ContainerStatus
|
||
|
|
:state="container.state as any"
|
||
|
|
:health="store.getHealthStatus(extractAppId(container.name)) as any"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="space-y-2 mb-4">
|
||
|
|
<div class="flex items-center justify-between text-sm">
|
||
|
|
<span class="text-white/60">Container ID</span>
|
||
|
|
<span class="text-white/80 font-mono text-xs">{{ container.id.substring(0, 12) }}</span>
|
||
|
|
</div>
|
||
|
|
<div class="flex items-center justify-between text-sm">
|
||
|
|
<span class="text-white/60">Created</span>
|
||
|
|
<span class="text-white/80 text-xs">{{ formatDate(container.created) }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex gap-2">
|
||
|
|
<button
|
||
|
|
v-if="container.state !== 'running'"
|
||
|
|
@click.stop="handleStart(extractAppId(container.name))"
|
||
|
|
class="flex-1 px-4 py-2 glass-button rounded text-sm font-medium text-white/90 hover:text-white transition-colors"
|
||
|
|
>
|
||
|
|
Start
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
v-else
|
||
|
|
@click.stop="handleStop(extractAppId(container.name))"
|
||
|
|
class="flex-1 px-4 py-2 glass-button rounded text-sm font-medium text-white/90 hover:text-white transition-colors"
|
||
|
|
>
|
||
|
|
Stop
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
@click.stop="handleRemove(extractAppId(container.name))"
|
||
|
|
class="px-4 py-2 glass-button rounded text-sm font-medium text-red-400/90 hover:text-red-400 transition-colors"
|
||
|
|
>
|
||
|
|
Remove
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Empty State -->
|
||
|
|
<div v-if="store.containers.length === 0" class="col-span-full glass-card p-12 text-center">
|
||
|
|
<svg class="w-16 h-16 mx-auto mb-4 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||
|
|
</svg>
|
||
|
|
<h3 class="text-xl font-semibold text-white mb-2">No containers found</h3>
|
||
|
|
<p class="text-white/60 mb-6">Install your first container app to get started</p>
|
||
|
|
<button
|
||
|
|
@click="$router.push('/dashboard/marketplace')"
|
||
|
|
class="px-6 py-3 glass-button rounded-lg font-medium text-white/90 hover:text-white transition-colors"
|
||
|
|
>
|
||
|
|
Browse Marketplace
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { onMounted } from 'vue'
|
||
|
|
import { useContainerStore } from '@/stores/container'
|
||
|
|
import ContainerStatus from '@/components/ContainerStatus.vue'
|
||
|
|
|
||
|
|
const store = useContainerStore()
|
||
|
|
|
||
|
|
onMounted(async () => {
|
||
|
|
await store.fetchContainers()
|
||
|
|
await store.fetchHealthStatus()
|
||
|
|
|
||
|
|
// Refresh every 30 seconds
|
||
|
|
setInterval(async () => {
|
||
|
|
await store.fetchContainers()
|
||
|
|
await store.fetchHealthStatus()
|
||
|
|
}, 30000)
|
||
|
|
})
|
||
|
|
|
||
|
|
function extractAppId(containerName: string): string {
|
||
|
|
// Extract app ID from container name like "archipelago-bitcoin-core"
|
||
|
|
return containerName.replace('archipelago-', '')
|
||
|
|
}
|
||
|
|
|
||
|
|
function extractAppName(containerName: string): string {
|
||
|
|
const appId = extractAppId(containerName)
|
||
|
|
// Convert kebab-case to Title Case
|
||
|
|
return appId
|
||
|
|
.split('-')
|
||
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||
|
|
.join(' ')
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatDate(dateString: string): string {
|
||
|
|
try {
|
||
|
|
const date = new Date(dateString)
|
||
|
|
return date.toLocaleDateString()
|
||
|
|
} catch {
|
||
|
|
return dateString
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleStart(appId: string) {
|
||
|
|
try {
|
||
|
|
await store.startContainer(appId)
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to start container:', e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleStop(appId: string) {
|
||
|
|
try {
|
||
|
|
await store.stopContainer(appId)
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to stop container:', e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleRemove(appId: string) {
|
||
|
|
if (!confirm(`Are you sure you want to remove ${appId}? This will delete the container.`)) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
await store.removeContainer(appId)
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to remove container:', e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|