feat(FN-2269): migrate dashboard session selection to global settings
- Add global settings schema/types fields for persisted dashboard session state (selected project and node) - Refactor NodeContext and current-project hooks to read/write project and node selection via global settings instead of project-local state - Update App wiring to use the new selection flow across dashboard startup and switching behavior - Add and expand dashboard tests for NodeContext, useCurrentProject, and view-state persistence behavior
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
/**
|
||||
* NodeContext provides React context for tracking which node the dashboard is currently viewing.
|
||||
* This enables seamless routing of API calls through the proxy when viewing remote nodes.
|
||||
* Persists the selected node ID to global settings (server-backed) instead of localStorage.
|
||||
*/
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import type { NodeConfig } from "@fusion/core";
|
||||
import { fetchGlobalSettings, updateGlobalSettings } from "../api";
|
||||
|
||||
const STORAGE_KEY = "fusion-dashboard-current-node";
|
||||
// Legacy localStorage key for migration - no longer used as primary storage
|
||||
const LEGACY_STORAGE_KEY = "fusion-dashboard-current-node";
|
||||
|
||||
export interface NodeContextValue {
|
||||
/** Currently selected node or null if viewing local node */
|
||||
@@ -29,43 +32,93 @@ export interface NodeProviderProps {
|
||||
|
||||
/**
|
||||
* Provider component that manages the current node state.
|
||||
* Persists the selected nodeId to localStorage and derives isRemote from node type.
|
||||
* Persists the selected nodeId to global settings and derives isRemote from node type.
|
||||
* This enables PWA fresh sessions to restore the correct node context.
|
||||
*/
|
||||
export function NodeProvider({ children }: NodeProviderProps) {
|
||||
const [currentNode, setCurrentNodeState] = useState<NodeConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// Track if we've hydrated from global settings
|
||||
const hydratedRef = useRef(false);
|
||||
// Cache of current settings to avoid repeated fetches
|
||||
const settingsCacheRef = useRef<string | undefined>(undefined);
|
||||
|
||||
// Load from localStorage on mount
|
||||
// Load from global settings on mount
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved) as NodeConfig;
|
||||
// Only restore if it's a remote node
|
||||
if (parsed && parsed.type === "remote") {
|
||||
setCurrentNodeState(parsed);
|
||||
let cancelled = false;
|
||||
|
||||
async function loadFromGlobalSettings() {
|
||||
try {
|
||||
const settings = await fetchGlobalSettings();
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
// Cache the current node ID
|
||||
settingsCacheRef.current = settings.dashboardCurrentNodeId;
|
||||
|
||||
const savedNodeId = settings.dashboardCurrentNodeId;
|
||||
if (savedNodeId) {
|
||||
// We don't have the full NodeConfig from settings, just the ID
|
||||
// The App.tsx will resolve the full node from the nodes list
|
||||
// We just need to indicate that a remote node is selected
|
||||
hydratedRef.current = true;
|
||||
}
|
||||
|
||||
// Also migrate legacy localStorage if no global settings entry exists
|
||||
if (!savedNodeId) {
|
||||
try {
|
||||
const legacy = localStorage.getItem(LEGACY_STORAGE_KEY);
|
||||
if (legacy) {
|
||||
const parsed = JSON.parse(legacy) as NodeConfig;
|
||||
if (parsed?.id && parsed?.type === "remote") {
|
||||
// Migrate to global settings
|
||||
settingsCacheRef.current = parsed.id;
|
||||
await updateGlobalSettings({ dashboardCurrentNodeId: parsed.id }).catch(() => {
|
||||
// Non-critical - migration failed
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore legacy localStorage errors
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Global settings fetch failed - non-critical
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
|
||||
loadFromGlobalSettings();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Persist remote node to localStorage
|
||||
// Sync remote node selection to global settings
|
||||
useEffect(() => {
|
||||
if (currentNode && currentNode.type === "remote") {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(currentNode));
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
if (loading) return;
|
||||
|
||||
// Only persist if we've hydrated and have a remote node
|
||||
if (currentNode && currentNode.type === "remote" && currentNode.id) {
|
||||
const newId = currentNode.id;
|
||||
if (settingsCacheRef.current !== newId) {
|
||||
settingsCacheRef.current = newId;
|
||||
updateGlobalSettings({ dashboardCurrentNodeId: newId }).catch(() => {
|
||||
// Non-critical - persistence failed
|
||||
});
|
||||
}
|
||||
} else if (currentNode === null && settingsCacheRef.current !== undefined) {
|
||||
// Clear the node selection
|
||||
settingsCacheRef.current = undefined;
|
||||
updateGlobalSettings({ dashboardCurrentNodeId: undefined }).catch(() => {
|
||||
// Non-critical - persistence failed
|
||||
});
|
||||
}
|
||||
}, [currentNode]);
|
||||
}, [currentNode, loading]);
|
||||
|
||||
const setCurrentNode = useCallback((node: NodeConfig | null) => {
|
||||
setCurrentNodeState(node);
|
||||
@@ -73,11 +126,11 @@ export function NodeProvider({ children }: NodeProviderProps) {
|
||||
|
||||
const clearCurrentNode = useCallback(() => {
|
||||
setCurrentNodeState(null);
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
// Clear from cache immediately for responsiveness
|
||||
settingsCacheRef.current = undefined;
|
||||
updateGlobalSettings({ dashboardCurrentNodeId: undefined }).catch(() => {
|
||||
// Non-critical - persistence failed
|
||||
});
|
||||
}, []);
|
||||
|
||||
const value: NodeContextValue = {
|
||||
|
||||
Reference in New Issue
Block a user