mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 03:33:53 +00:00
feat: add grouping toggle for flat list view
adds a grouped/list toggle to the dashboard that allows users to switch between: - grouped view: vms/containers grouped by node (default) - list view: flat list of all vms/containers across cluster in list view, users can sort by any column to identify top resource consumers across the entire cluster, useful for monitoring high bandwidth/cpu/memory usage addresses #370
This commit is contained in:
@@ -20,6 +20,7 @@ interface DashboardProps {
|
||||
|
||||
type ViewMode = 'all' | 'vm' | 'lxc';
|
||||
type StatusMode = 'all' | 'running' | 'stopped';
|
||||
type GroupingMode = 'grouped' | 'flat';
|
||||
|
||||
|
||||
export function Dashboard(props: DashboardProps) {
|
||||
@@ -56,6 +57,12 @@ export function Dashboard(props: DashboardProps) {
|
||||
(storedStatusMode === 'all' || storedStatusMode === 'running' || storedStatusMode === 'stopped') ? storedStatusMode : 'all'
|
||||
);
|
||||
|
||||
// Grouping mode - grouped by node or flat list
|
||||
const storedGroupingMode = localStorage.getItem('dashboardGroupingMode');
|
||||
const [groupingMode, setGroupingMode] = createSignal<GroupingMode>(
|
||||
(storedGroupingMode === 'grouped' || storedGroupingMode === 'flat') ? storedGroupingMode : 'grouped'
|
||||
);
|
||||
|
||||
const [showFilters, setShowFilters] = createSignal(
|
||||
localStorage.getItem('dashboardShowFilters') !== null
|
||||
? localStorage.getItem('dashboardShowFilters') === 'true'
|
||||
@@ -91,6 +98,10 @@ export function Dashboard(props: DashboardProps) {
|
||||
localStorage.setItem('dashboardStatusMode', statusMode());
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
localStorage.setItem('dashboardGroupingMode', groupingMode());
|
||||
});
|
||||
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
@@ -241,10 +252,64 @@ export function Dashboard(props: DashboardProps) {
|
||||
return guests;
|
||||
});
|
||||
|
||||
// Group by node
|
||||
// Group by node or return flat list based on grouping mode
|
||||
const groupedGuests = createMemo(() => {
|
||||
const guests = filteredGuests();
|
||||
|
||||
// If flat mode, return all guests in a single group
|
||||
if (groupingMode() === 'flat') {
|
||||
const groups: Record<string, (VM | Container)[]> = { '': guests };
|
||||
// Sort the flat list
|
||||
const key = sortKey();
|
||||
const dir = sortDirection();
|
||||
if (key) {
|
||||
groups[''] = groups[''].sort((a, b) => {
|
||||
let aVal: string | number | boolean | null | undefined = a[key] as string | number | boolean | null | undefined;
|
||||
let bVal: string | number | boolean | null | undefined = b[key] as string | number | boolean | null | undefined;
|
||||
|
||||
// Special handling for percentage-based columns
|
||||
if (key === 'cpu') {
|
||||
// CPU is displayed as percentage
|
||||
aVal = a.cpu * 100;
|
||||
bVal = b.cpu * 100;
|
||||
} else if (key === 'memory') {
|
||||
// Memory is displayed as percentage (use pre-calculated usage)
|
||||
aVal = a.memory ? (a.memory.usage || 0) : 0;
|
||||
bVal = b.memory ? (b.memory.usage || 0) : 0;
|
||||
} else if (key === 'disk') {
|
||||
// Disk is displayed as percentage
|
||||
aVal = a.disk.total > 0 ? (a.disk.used / a.disk.total) * 100 : 0;
|
||||
bVal = b.disk.total > 0 ? (b.disk.used / b.disk.total) * 100 : 0;
|
||||
}
|
||||
|
||||
// Handle null/undefined/empty values - put at end for both asc and desc
|
||||
const aIsEmpty = aVal === null || aVal === undefined || aVal === '';
|
||||
const bIsEmpty = bVal === null || bVal === undefined || bVal === '';
|
||||
|
||||
if (aIsEmpty && bIsEmpty) return 0;
|
||||
if (aIsEmpty) return 1;
|
||||
if (bIsEmpty) return -1;
|
||||
|
||||
// Type-specific value preparation
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
// Numeric comparison
|
||||
const comparison = aVal < bVal ? -1 : 1;
|
||||
return dir === 'asc' ? comparison : -comparison;
|
||||
} else {
|
||||
// String comparison (case-insensitive)
|
||||
const aStr = String(aVal).toLowerCase();
|
||||
const bStr = String(bVal).toLowerCase();
|
||||
|
||||
if (aStr === bStr) return 0;
|
||||
const comparison = aStr < bStr ? -1 : 1;
|
||||
return dir === 'asc' ? comparison : -comparison;
|
||||
}
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
// Original grouped by node logic
|
||||
const groups: Record<string, (VM | Container)[]> = {};
|
||||
guests.forEach(guest => {
|
||||
if (!groups[guest.node]) {
|
||||
@@ -489,6 +554,8 @@ export function Dashboard(props: DashboardProps) {
|
||||
setViewMode={setViewMode}
|
||||
statusMode={statusMode}
|
||||
setStatusMode={setStatusMode}
|
||||
groupingMode={groupingMode}
|
||||
setGroupingMode={setGroupingMode}
|
||||
setSortKey={setSortKey}
|
||||
setSortDirection={setSortDirection}
|
||||
searchInputRef={(el) => searchInputRef = el}
|
||||
@@ -631,7 +698,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
<For each={Object.entries(groupedGuests()).sort(([a], [b]) => a.localeCompare(b))} fallback={<></>}>
|
||||
{([node, guests]) => (
|
||||
<>
|
||||
<Show when={node}>
|
||||
<Show when={node && groupingMode() === 'grouped'}>
|
||||
<tr class="bg-gray-50/50 dark:bg-gray-700/30">
|
||||
<td class="p-1 px-2 text-xs font-medium text-gray-600 dark:text-gray-400 w-[200px]">
|
||||
<a
|
||||
@@ -655,7 +722,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
return (
|
||||
<GuestRow
|
||||
guest={guest}
|
||||
showNode={false}
|
||||
showNode={groupingMode() === 'flat'}
|
||||
alertStyles={getAlertStyles(guestId, activeAlerts)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,8 @@ interface DashboardFilterProps {
|
||||
setViewMode: (value: 'all' | 'vm' | 'lxc') => void;
|
||||
statusMode: () => 'all' | 'running' | 'stopped';
|
||||
setStatusMode: (value: 'all' | 'running' | 'stopped') => void;
|
||||
groupingMode: () => 'grouped' | 'flat';
|
||||
setGroupingMode: (value: 'grouped' | 'flat') => void;
|
||||
setSortKey: (value: string) => void;
|
||||
setSortDirection: (value: string) => void;
|
||||
searchInputRef?: (el: HTMLInputElement) => void;
|
||||
@@ -146,6 +148,34 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
||||
|
||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||
|
||||
{/* Grouping Mode Toggle */}
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
<button type="button"
|
||||
onClick={() => props.setGroupingMode('grouped')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
|
||||
props.groupingMode() === 'grouped'
|
||||
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
title="Group by node"
|
||||
>
|
||||
Grouped
|
||||
</button>
|
||||
<button type="button"
|
||||
onClick={() => props.setGroupingMode('flat')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
|
||||
props.groupingMode() === 'flat'
|
||||
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
title="Flat list view"
|
||||
>
|
||||
List
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||
|
||||
{/* Reset Button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -154,6 +184,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
||||
props.setSortDirection('asc');
|
||||
props.setViewMode('all');
|
||||
props.setStatusMode('all');
|
||||
props.setGroupingMode('grouped');
|
||||
}}
|
||||
title="Reset all filters"
|
||||
class="flex items-center justify-center px-2.5 py-1 text-xs font-medium text-gray-600 dark:text-gray-400
|
||||
@@ -170,7 +201,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
||||
</button>
|
||||
|
||||
{/* Active Indicator */}
|
||||
<Show when={props.search() || props.viewMode() !== 'all' || props.statusMode() !== 'all'}>
|
||||
<Show when={props.search() || props.viewMode() !== 'all' || props.statusMode() !== 'all' || props.groupingMode() !== 'grouped'}>
|
||||
<span class="text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 px-2 py-0.5 rounded-full font-medium">
|
||||
Active
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user