mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-02 21:19:05 +00:00
210 lines
5.1 KiB
TypeScript
210 lines
5.1 KiB
TypeScript
import { writable, get } from 'svelte/store';
|
|
import type { EnvironmentStats } from '../../routes/api/dashboard/stats/+server';
|
|
|
|
// Grid item layout format for svelte-grid
|
|
export interface GridItem {
|
|
id: number;
|
|
x: number;
|
|
y: number;
|
|
w: number;
|
|
h: number;
|
|
[key: string]: unknown; // Allow svelte-grid internal properties
|
|
}
|
|
|
|
export interface DashboardPreferences {
|
|
gridLayout: GridItem[];
|
|
}
|
|
|
|
const defaultPreferences: DashboardPreferences = {
|
|
gridLayout: []
|
|
};
|
|
|
|
// Environment info from API
|
|
interface EnvironmentInfo {
|
|
id: number;
|
|
name: string;
|
|
host?: string;
|
|
icon: string;
|
|
socketPath?: string;
|
|
connectionType?: 'socket' | 'direct' | 'hawser-standard' | 'hawser-edge';
|
|
}
|
|
|
|
// Metrics history point for charts
|
|
export interface MetricsHistoryPoint {
|
|
cpu_percent: number;
|
|
memory_percent: number;
|
|
timestamp: string;
|
|
}
|
|
|
|
// Tile item combining environment info and stats
|
|
export interface TileItem {
|
|
id: number;
|
|
stats: EnvironmentStats | null;
|
|
info: EnvironmentInfo | null;
|
|
loading: boolean;
|
|
}
|
|
|
|
// Dashboard data store for caching between navigations
|
|
export interface DashboardData {
|
|
tiles: TileItem[];
|
|
gridItems: GridItem[];
|
|
lastFetchTime: number | null;
|
|
initialized: boolean;
|
|
}
|
|
|
|
const defaultDashboardData: DashboardData = {
|
|
tiles: [],
|
|
gridItems: [],
|
|
lastFetchTime: null,
|
|
initialized: false
|
|
};
|
|
|
|
function createDashboardDataStore() {
|
|
const { subscribe, set, update } = writable<DashboardData>(defaultDashboardData);
|
|
|
|
return {
|
|
subscribe,
|
|
setTiles: (tiles: TileItem[]) => {
|
|
update(data => ({ ...data, tiles, lastFetchTime: Date.now() }));
|
|
},
|
|
updateTile: (id: number, updates: Partial<TileItem>) => {
|
|
update(data => ({
|
|
...data,
|
|
tiles: data.tiles.map(t => t.id === id ? { ...t, ...updates } : t),
|
|
lastFetchTime: Date.now()
|
|
}));
|
|
},
|
|
// Partial update for progressive loading - merges into existing stats
|
|
updateTilePartial: (id: number, partialStats: Partial<EnvironmentStats>) => {
|
|
update(data => ({
|
|
...data,
|
|
tiles: data.tiles.map(t => {
|
|
if (t.id === id && t.stats) {
|
|
return {
|
|
...t,
|
|
stats: {
|
|
...t.stats,
|
|
...partialStats
|
|
}
|
|
};
|
|
}
|
|
return t;
|
|
}),
|
|
lastFetchTime: Date.now()
|
|
}));
|
|
},
|
|
setGridItems: (gridItems: GridItem[]) => {
|
|
update(data => ({ ...data, gridItems }));
|
|
},
|
|
setInitialized: (initialized: boolean) => {
|
|
update(data => ({ ...data, initialized }));
|
|
},
|
|
markAllLoading: () => {
|
|
update(data => ({
|
|
...data,
|
|
tiles: data.tiles.map(t => ({ ...t, loading: true }))
|
|
}));
|
|
},
|
|
// Invalidate cache to force a fresh fetch on next dashboard visit
|
|
// Clear tiles so dashboard starts fresh with new data
|
|
invalidate: () => {
|
|
update(data => ({
|
|
...data,
|
|
tiles: [],
|
|
lastFetchTime: null,
|
|
initialized: false
|
|
}));
|
|
},
|
|
reset: () => set(defaultDashboardData),
|
|
getData: () => get({ subscribe })
|
|
};
|
|
}
|
|
|
|
export const dashboardData = createDashboardDataStore();
|
|
|
|
// Number of columns in the grid
|
|
export const GRID_COLS = 4;
|
|
// Row height for tiles - compact tiles (h=1) show basic info, larger tiles show more
|
|
// At height=2 (default), should fit: header, container counts, health, CPU/mem, resources, events
|
|
export const GRID_ROW_HEIGHT = 175;
|
|
|
|
function createDashboardStore() {
|
|
const { subscribe, set, update } = writable<DashboardPreferences>(defaultPreferences);
|
|
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
let initialized = false;
|
|
|
|
async function load() {
|
|
try {
|
|
const response = await fetch('/api/dashboard/preferences');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
// Handle migration from old format
|
|
if (data.gridLayout && Array.isArray(data.gridLayout)) {
|
|
set({ gridLayout: data.gridLayout });
|
|
} else {
|
|
set({ gridLayout: [] });
|
|
}
|
|
} else {
|
|
set({ gridLayout: [] });
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load dashboard preferences:', error);
|
|
set({ gridLayout: [] });
|
|
} finally {
|
|
// Always mark as initialized so saves can proceed
|
|
initialized = true;
|
|
}
|
|
}
|
|
|
|
async function save(prefs: DashboardPreferences) {
|
|
try {
|
|
await fetch('/api/dashboard/preferences', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(prefs)
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to save dashboard preferences:', error);
|
|
}
|
|
}
|
|
|
|
// Debounced save - auto-saves 500ms after last change
|
|
function scheduleSave(prefs: DashboardPreferences) {
|
|
if (saveTimeout) {
|
|
clearTimeout(saveTimeout);
|
|
}
|
|
saveTimeout = setTimeout(() => {
|
|
save(prefs);
|
|
saveTimeout = null;
|
|
}, 500);
|
|
}
|
|
|
|
return {
|
|
subscribe,
|
|
load,
|
|
setGridLayout: (layout: GridItem[]) => {
|
|
update(prefs => {
|
|
// Only keep essential properties to avoid storing internal svelte-grid state
|
|
const cleanLayout = layout.map(item => ({
|
|
id: item.id,
|
|
x: item.x,
|
|
y: item.y,
|
|
w: item.w,
|
|
h: item.h
|
|
}));
|
|
const newPrefs = { ...prefs, gridLayout: cleanLayout };
|
|
if (initialized) {
|
|
scheduleSave(newPrefs);
|
|
}
|
|
return newPrefs;
|
|
});
|
|
},
|
|
reset: () => {
|
|
initialized = false;
|
|
set(defaultPreferences);
|
|
}
|
|
};
|
|
}
|
|
|
|
export const dashboardPreferences = createDashboardStore();
|