mirror of
https://github.com/unraid/api.git
synced 2026-01-04 07:29:48 -06:00
feat: use and customize NavigationMenu and update status badges
This commit is contained in:
@@ -1,13 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import {
|
||||
UBadge,
|
||||
UButton,
|
||||
UCheckbox,
|
||||
UDropdownMenu,
|
||||
UIcon,
|
||||
UNavigationMenu,
|
||||
USwitch,
|
||||
UTabs,
|
||||
} from '#components';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
import { UIcon, UBadge, UTabs } from '#components';
|
||||
|
||||
interface NavigationItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
slot?: string;
|
||||
status?: {
|
||||
label: string;
|
||||
dotColor: string; // Tailwind color class like 'bg-green-500'
|
||||
}[];
|
||||
children?: NavigationItem[]; // For grouped/nested items
|
||||
isGroup?: boolean; // Indicates if this is a group/folder
|
||||
}
|
||||
|
||||
interface NavigationMenuItem extends NavigationItem {
|
||||
to?: string;
|
||||
slot?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface TabItem {
|
||||
@@ -34,11 +58,58 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const selectedNavigationId = ref(props.defaultNavigationId || props.navigationItems[0]?.id || '');
|
||||
const selectedTab = ref(props.defaultTabKey || '0');
|
||||
const selectedItems = ref<string[]>([]);
|
||||
|
||||
const selectedNavigationItem = computed(() =>
|
||||
props.navigationItems.find((item) => item.id === selectedNavigationId.value)
|
||||
const selectedNavigationItem = computed(() => {
|
||||
// First check top-level items
|
||||
const topLevel = props.navigationItems.find((item) => item.id === selectedNavigationId.value);
|
||||
if (topLevel) return topLevel;
|
||||
|
||||
// Then check nested items
|
||||
for (const item of props.navigationItems) {
|
||||
if (item.children) {
|
||||
const nested = item.children.find((child) => child.id === selectedNavigationId.value);
|
||||
if (nested) return nested;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const navigationMenuItems = computed((): NavigationMenuItem[] =>
|
||||
props.navigationItems.map((item) => ({
|
||||
label: item.label,
|
||||
icon: item.icon,
|
||||
id: item.id,
|
||||
badge: item.badge,
|
||||
slot: item.slot,
|
||||
onClick: () => selectNavigationItem(item.id),
|
||||
children: item.children?.map((child) => ({
|
||||
label: child.label,
|
||||
icon: child.icon,
|
||||
id: child.id,
|
||||
badge: child.badge,
|
||||
slot: child.slot,
|
||||
onClick: () => selectNavigationItem(child.id),
|
||||
status: child.status,
|
||||
})),
|
||||
isGroup: item.isGroup,
|
||||
}))
|
||||
);
|
||||
|
||||
const toggleItemSelection = (itemId: string) => {
|
||||
const index = selectedItems.value.indexOf(itemId);
|
||||
if (index > -1) {
|
||||
selectedItems.value.splice(index, 1);
|
||||
} else {
|
||||
selectedItems.value.push(itemId);
|
||||
}
|
||||
};
|
||||
|
||||
const isItemSelected = (itemId: string) => {
|
||||
return selectedItems.value.includes(itemId);
|
||||
};
|
||||
|
||||
const tabItems = computed(() =>
|
||||
props.tabs.map((tab) => ({
|
||||
label: tab.label,
|
||||
@@ -74,28 +145,75 @@ const getCurrentTabProps = () => {
|
||||
<div class="flex h-full gap-6">
|
||||
<!-- Left Navigation Section -->
|
||||
<div class="w-64 flex-shrink-0">
|
||||
<nav class="space-y-1">
|
||||
<button
|
||||
v-for="item in navigationItems"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'group flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
selectedNavigationId === item.id
|
||||
? 'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white'
|
||||
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white',
|
||||
]"
|
||||
@click="selectNavigationItem(item.id)"
|
||||
>
|
||||
<UIcon v-if="item.icon" :name="item.icon" class="h-5 w-5 flex-shrink-0" />
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
<UBadge v-if="item.badge" size="xs" :label="String(item.badge)" class="ml-auto" />
|
||||
</button>
|
||||
</nav>
|
||||
<UNavigationMenu :items="navigationMenuItems" orientation="vertical">
|
||||
<template v-for="navItem in navigationMenuItems" :key="navItem.id" #[navItem.slot]>
|
||||
<div class="flex items-center gap-3">
|
||||
<UCheckbox
|
||||
:model-value="isItemSelected(navItem.id)"
|
||||
class="flex-shrink-0"
|
||||
@update:model-value="toggleItemSelection(navItem.id)"
|
||||
@click.stop
|
||||
/>
|
||||
<UIcon v-if="navItem.icon" :name="navItem.icon" class="h-5 w-5 flex-shrink-0" />
|
||||
<span class="truncate flex-1">{{ navItem.label }}</span>
|
||||
<UBadge v-if="navItem.badge" size="xs" :label="String(navItem.badge)" />
|
||||
</div>
|
||||
</template>
|
||||
</UNavigationMenu>
|
||||
</div>
|
||||
|
||||
<!-- Right Content Section -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<UTabs v-model="selectedTab" :items="tabItems" class="w-full" />
|
||||
<div v-if="selectedNavigationItem" class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<UIcon
|
||||
v-if="selectedNavigationItem.icon"
|
||||
:name="selectedNavigationItem.icon"
|
||||
class="h-8 w-8"
|
||||
/>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{{ selectedNavigationItem.label }}
|
||||
</h1>
|
||||
|
||||
<!-- Status Indicators -->
|
||||
<div v-if="selectedNavigationItem.status" class="flex items-center gap-4">
|
||||
<UBadge
|
||||
v-for="(statusItem, index) in selectedNavigationItem.status"
|
||||
:key="index"
|
||||
variant="subtle"
|
||||
color="neutral"
|
||||
size="sm"
|
||||
>
|
||||
<div :class="['h-2 w-2 rounded-full mr-2', statusItem.dotColor]"/>
|
||||
{{ statusItem.label }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side Controls -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-medium">Autostart</span>
|
||||
<USwitch :model-value="true" />
|
||||
</div>
|
||||
|
||||
<!-- Manage Dropdown -->
|
||||
<UDropdownMenu
|
||||
:items="[
|
||||
[{ label: 'Edit', icon: 'i-lucide-edit' }],
|
||||
[{ label: 'Remove', icon: 'i-lucide-trash-2' }],
|
||||
[{ label: 'Restart', icon: 'i-lucide-refresh-cw' }],
|
||||
[{ label: 'Force Update', icon: 'i-lucide-download' }],
|
||||
]"
|
||||
>
|
||||
<UButton variant="outline" color="primary" trailing-icon="i-lucide-chevron-down">
|
||||
Manage
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UTabs v-model="selectedTab" variant="link" :items="tabItems" class="w-full" />
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="mt-6">
|
||||
|
||||
@@ -28,34 +28,69 @@ const dockerContainers = [
|
||||
id: 'immich',
|
||||
label: 'immich',
|
||||
icon: 'i-lucide-play-circle',
|
||||
slot: 'immich' as const,
|
||||
status: [
|
||||
{ label: 'Update available', dotColor: 'bg-orange-500' },
|
||||
{ label: 'Started', dotColor: 'bg-green-500' }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'organizrv2',
|
||||
label: 'organizrv2',
|
||||
icon: 'i-lucide-layers',
|
||||
slot: 'organizrv2' as const,
|
||||
status: [
|
||||
{ label: 'Started', dotColor: 'bg-green-500' }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'jellyfin',
|
||||
label: 'Jellyfin',
|
||||
icon: 'i-lucide-film',
|
||||
slot: 'jellyfin' as const,
|
||||
status: [
|
||||
{ label: 'Stopped', dotColor: 'bg-red-500' }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mongodb',
|
||||
label: 'MongoDB',
|
||||
icon: 'i-lucide-database',
|
||||
badge: 'DB',
|
||||
},
|
||||
{
|
||||
id: 'postgres17',
|
||||
label: 'postgres17',
|
||||
icon: 'i-lucide-database',
|
||||
badge: 'DB',
|
||||
},
|
||||
{
|
||||
id: 'redis',
|
||||
label: 'Redis',
|
||||
icon: 'i-lucide-database',
|
||||
badge: 'DB',
|
||||
id: 'databases',
|
||||
label: 'Databases',
|
||||
icon: 'i-lucide-folder-database',
|
||||
slot: 'databases' as const,
|
||||
isGroup: true,
|
||||
children: [
|
||||
{
|
||||
id: 'mongodb',
|
||||
label: 'MongoDB',
|
||||
icon: 'i-lucide-database',
|
||||
badge: 'DB',
|
||||
slot: 'mongodb' as const,
|
||||
status: [
|
||||
{ label: 'Started', dotColor: 'bg-green-500' }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'postgres17',
|
||||
label: 'postgres17',
|
||||
icon: 'i-lucide-database',
|
||||
badge: 'DB',
|
||||
slot: 'postgres17' as const,
|
||||
status: [
|
||||
{ label: 'Update available', dotColor: 'bg-orange-500' },
|
||||
{ label: 'Paused', dotColor: 'bg-blue-500' }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'redis',
|
||||
label: 'Redis',
|
||||
icon: 'i-lucide-database',
|
||||
badge: 'DB',
|
||||
slot: 'redis' as const,
|
||||
status: [
|
||||
{ label: 'Started', dotColor: 'bg-green-500' }
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user