feat: Add Plugin Configs landing page — collapse 9 sidebar items to 1
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Replace individual plugin config sidebar entries with a single "Plugin Configs" link that opens a card-based landing page. Cards show status (Active/Configured/ Not Configured), config count, and link to existing editor views. Search bar for filtering. All existing plugin routes preserved for direct navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,13 +27,6 @@ import {
|
||||
AlertTriangle,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
Crosshair,
|
||||
Navigation2,
|
||||
Pickaxe,
|
||||
DoorOpen,
|
||||
Gift,
|
||||
Flame,
|
||||
Swords,
|
||||
Menu,
|
||||
X,
|
||||
} from 'lucide-vue-next'
|
||||
@@ -67,15 +60,7 @@ const navSections: NavSection[] = [
|
||||
{
|
||||
label: 'Plugin Configs',
|
||||
items: [
|
||||
{ name: 'Loot Builder', path: '/loot-builder', icon: Crosshair, permission: 'loot.view' },
|
||||
{ name: 'Teleport', path: '/teleport-config', icon: Navigation2, permission: 'teleport.view' },
|
||||
{ name: 'Gather Rates', path: '/gather-manager', icon: Pickaxe, permission: 'gather.view' },
|
||||
{ name: 'Auto Doors', path: '/autodoors', icon: DoorOpen, permission: 'autodoors.view' },
|
||||
{ name: 'Kits', path: '/kits', icon: Gift, permission: 'kits.view' },
|
||||
{ name: 'Furnace Splitter', path: '/furnace-splitter', icon: Flame, permission: 'furnacesplitter.view' },
|
||||
{ name: 'Better Chat', path: '/better-chat', icon: MessageSquare, permission: 'betterchat.view' },
|
||||
{ name: 'Timed Execute', path: '/timed-execute', icon: Clock, permission: 'timedexecute.view' },
|
||||
{ name: 'Raidable Bases', path: '/raidable-bases', icon: Swords, permission: 'raidablebases.view' },
|
||||
{ name: 'Plugin Configs', path: '/plugin-configs', icon: Puzzle, permission: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -110,6 +110,11 @@ const panelRoutes: RouteRecordRaw[] = [
|
||||
name: 'files',
|
||||
component: () => import('@/views/admin/FileManagerView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'plugin-configs',
|
||||
name: 'plugin-configs',
|
||||
component: () => import('@/views/admin/PluginConfigsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'loot-builder',
|
||||
name: 'loot-builder',
|
||||
|
||||
185
frontend/src/views/admin/PluginConfigsView.vue
Normal file
185
frontend/src/views/admin/PluginConfigsView.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useLootStore } from '@/stores/loot'
|
||||
import { useTeleportStore } from '@/stores/teleport'
|
||||
import { useGatherStore } from '@/stores/gather'
|
||||
import { useAutoDoorsStore } from '@/stores/autodoors'
|
||||
import { useKitsStore } from '@/stores/kits'
|
||||
import { useFurnaceSplitterStore } from '@/stores/furnacesplitter'
|
||||
import { useBetterChatStore } from '@/stores/betterchat'
|
||||
import { useTimedExecuteStore } from '@/stores/timedexecute'
|
||||
import { useRaidableBasesStore } from '@/stores/raidablebases'
|
||||
import {
|
||||
Crosshair,
|
||||
Navigation2,
|
||||
Pickaxe,
|
||||
DoorOpen,
|
||||
Gift,
|
||||
Flame,
|
||||
MessageSquare,
|
||||
Clock,
|
||||
Swords,
|
||||
Search,
|
||||
ArrowRight,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const lootStore = useLootStore()
|
||||
const teleportStore = useTeleportStore()
|
||||
const gatherStore = useGatherStore()
|
||||
const autoDoorsStore = useAutoDoorsStore()
|
||||
const kitsStore = useKitsStore()
|
||||
const furnaceSplitterStore = useFurnaceSplitterStore()
|
||||
const betterChatStore = useBetterChatStore()
|
||||
const timedExecuteStore = useTimedExecuteStore()
|
||||
const raidableBasesStore = useRaidableBasesStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const loading = ref(true)
|
||||
|
||||
interface PluginDef {
|
||||
key: string
|
||||
name: string
|
||||
description: string
|
||||
icon: any
|
||||
path: string
|
||||
permission: string
|
||||
getConfigs: () => any[]
|
||||
fetchFn: () => Promise<void>
|
||||
}
|
||||
|
||||
const plugins: PluginDef[] = [
|
||||
{ key: 'loot', name: 'Loot Tables', description: 'Configure loot container drop tables and item probabilities', icon: Crosshair, path: '/loot-builder', permission: 'loot.view', getConfigs: () => lootStore.profiles, fetchFn: () => lootStore.fetchProfiles() },
|
||||
{ key: 'teleport', name: 'Teleport', description: 'Home locations, TPR cooldowns, and VIP teleport settings', icon: Navigation2, path: '/teleport-config', permission: 'teleport.view', getConfigs: () => teleportStore.configs, fetchFn: () => teleportStore.fetchConfigs() },
|
||||
{ key: 'gather', name: 'Gather Rates', description: 'Resource gathering multipliers and pickup rates', icon: Pickaxe, path: '/gather-manager', permission: 'gather.view', getConfigs: () => gatherStore.configs, fetchFn: () => gatherStore.fetchConfigs() },
|
||||
{ key: 'autodoors', name: 'Auto Doors', description: 'Automatic door closing delays and permissions', icon: DoorOpen, path: '/autodoors', permission: 'autodoors.view', getConfigs: () => autoDoorsStore.configs, fetchFn: () => autoDoorsStore.fetchConfigs() },
|
||||
{ key: 'kits', name: 'Kits', description: 'Player kits with items, cooldowns, and permissions', icon: Gift, path: '/kits', permission: 'kits.view', getConfigs: () => kitsStore.configs, fetchFn: () => kitsStore.fetchConfigs() },
|
||||
{ key: 'furnacesplitter', name: 'Furnace Splitter', description: 'Automatic furnace ore splitting and smelting config', icon: Flame, path: '/furnace-splitter', permission: 'furnacesplitter.view', getConfigs: () => furnaceSplitterStore.configs, fetchFn: () => furnaceSplitterStore.fetchConfigs() },
|
||||
{ key: 'betterchat', name: 'Better Chat', description: 'Chat formatting, group colors, and title prefixes', icon: MessageSquare, path: '/better-chat', permission: 'betterchat.view', getConfigs: () => betterChatStore.configs, fetchFn: () => betterChatStore.fetchConfigs() },
|
||||
{ key: 'timedexecute', name: 'Timed Execute', description: 'Scheduled, real-time, and event-driven command execution', icon: Clock, path: '/timed-execute', permission: 'timedexecute.view', getConfigs: () => timedExecuteStore.configs, fetchFn: () => timedExecuteStore.fetchConfigs() },
|
||||
{ key: 'raidablebases', name: 'Raidable Bases', description: 'PVE raid events, difficulty, NPCs, and loot settings', icon: Swords, path: '/raidable-bases', permission: 'raidablebases.view', getConfigs: () => raidableBasesStore.configs, fetchFn: () => raidableBasesStore.fetchConfigs() },
|
||||
]
|
||||
|
||||
const visiblePlugins = computed(() =>
|
||||
plugins.filter(p => auth.hasPermission(p.permission))
|
||||
)
|
||||
|
||||
const filteredPlugins = computed(() => {
|
||||
if (!searchQuery.value.trim()) return visiblePlugins.value
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
return visiblePlugins.value.filter(
|
||||
p => p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
function getStatus(plugin: PluginDef): { label: string; color: string } {
|
||||
const configs = plugin.getConfigs()
|
||||
if (!configs || configs.length === 0) return { label: 'Not Configured', color: 'neutral' }
|
||||
const hasActive = configs.some((c: any) => c.is_active)
|
||||
if (hasActive) return { label: 'Active', color: 'green' }
|
||||
return { label: 'Configured', color: 'blue' }
|
||||
}
|
||||
|
||||
function getConfigCount(plugin: PluginDef): string {
|
||||
const configs = plugin.getConfigs()
|
||||
if (!configs || configs.length === 0) return 'No profiles'
|
||||
return `${configs.length} profile${configs.length !== 1 ? 's' : ''}`
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const fetches = visiblePlugins.value.map(p => p.fetchFn().catch(() => {}))
|
||||
await Promise.all(fetches)
|
||||
loading.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 max-w-7xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Plugin Configs</h1>
|
||||
<p class="text-neutral-400 mt-1">Configure and manage your server plugins</p>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="relative mb-6">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-500" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search plugins..."
|
||||
class="w-full pl-10 pr-4 py-2 bg-neutral-900 border border-neutral-800 rounded-lg text-sm text-neutral-200 placeholder-neutral-500 focus:outline-none focus:border-oxide-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="i in visiblePlugins.length"
|
||||
:key="i"
|
||||
class="bg-neutral-900 border border-neutral-800 rounded-lg p-5 animate-pulse"
|
||||
>
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="w-10 h-10 bg-neutral-800 rounded-lg" />
|
||||
<div class="flex-1">
|
||||
<div class="h-4 w-24 bg-neutral-800 rounded" />
|
||||
<div class="h-3 w-16 bg-neutral-800 rounded mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-3 w-full bg-neutral-800 rounded mt-3" />
|
||||
<div class="h-3 w-2/3 bg-neutral-800 rounded mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards -->
|
||||
<div v-else-if="filteredPlugins.length" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="plugin in filteredPlugins"
|
||||
:key="plugin.key"
|
||||
class="bg-neutral-900 border border-neutral-800 rounded-lg p-5 hover:border-neutral-700 transition-colors group"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-neutral-800 rounded-lg flex items-center justify-center group-hover:bg-oxide-500/10 transition-colors">
|
||||
<component :is="plugin.icon" class="w-5 h-5 text-neutral-400 group-hover:text-oxide-400 transition-colors" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-white">{{ plugin.name }}</h3>
|
||||
<span class="text-xs text-neutral-500">{{ getConfigCount(plugin) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status badge -->
|
||||
<span
|
||||
class="text-[10px] font-medium px-2 py-0.5 rounded-full"
|
||||
:class="{
|
||||
'bg-green-500/10 text-green-400': getStatus(plugin).color === 'green',
|
||||
'bg-blue-500/10 text-blue-400': getStatus(plugin).color === 'blue',
|
||||
'bg-neutral-800 text-neutral-500': getStatus(plugin).color === 'neutral',
|
||||
}"
|
||||
>
|
||||
{{ getStatus(plugin).label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-neutral-400 mb-4 line-clamp-2">{{ plugin.description }}</p>
|
||||
|
||||
<button
|
||||
@click="router.push(plugin.path)"
|
||||
class="w-full flex items-center justify-center gap-2 px-3 py-2 bg-neutral-800 hover:bg-oxide-500/10 text-neutral-300 hover:text-oxide-400 text-xs font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Configure
|
||||
<ArrowRight class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else class="text-center py-12">
|
||||
<p class="text-neutral-500">No plugins match your search.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user