From 650c42077a6eab385e1bf0fa20e7a88c5ed9b247 Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 18 Apr 2026 04:30:33 -0700 Subject: [PATCH] [hAIk]: refactor: wire dashboardsSlice through backend-bridge, remove inline fetch calls from slice and update all 6 consumer files --- .../src/app/components/Layout/AppShell.tsx | 4 +- .../src/app/components/Layout/Sidebar.tsx | 8 +- .../Layout/hooks/useUrlInterception.ts | 6 +- .../Dashboard/hooks/useDashboardThumbnail.ts | 4 +- .../Dashboard/hooks/useToolbarActions.ts | 4 +- .../DashboardSelection/DashboardSelection.tsx | 26 ++-- .../shared/backend-bridge/apps/dashboards.ts | 40 +++--- frontend/src/shared/state/dashboardsSlice.ts | 124 +++--------------- 8 files changed, 71 insertions(+), 145 deletions(-) diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index bfbadc41..1bccee7d 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -8,7 +8,7 @@ import RestartAltIcon from '@mui/icons-material/RestartAlt'; import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'; import Settings from '@/app/pages/Settings/Settings'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -import { fetchDashboards } from '@/shared/state/dashboardsSlice'; +import { LIST_DASHBOARDS } from '@/shared/backend-bridge/apps/dashboards'; import { fetchOutputs } from '@/shared/state/outputsSlice'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useUpdateNotification } from './hooks/useUpdateNotification'; @@ -40,7 +40,7 @@ const AppShell: React.FC = () => { useUrlInterception(dashboardList); useEffect(() => { - dispatch(fetchDashboards()); + dispatch(LIST_DASHBOARDS()); dispatch(fetchOutputs()); }, [dispatch]); diff --git a/frontend/src/app/components/Layout/Sidebar.tsx b/frontend/src/app/components/Layout/Sidebar.tsx index a5a4ea0b..ca6ff2d7 100644 --- a/frontend/src/app/components/Layout/Sidebar.tsx +++ b/frontend/src/app/components/Layout/Sidebar.tsx @@ -20,7 +20,7 @@ import AddIcon from '@mui/icons-material/Add'; import SettingsIcon from '@mui/icons-material/Settings'; import ExtensionIcon from '@mui/icons-material/Extension'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -import { createDashboard, renameDashboard } from '@/shared/state/dashboardsSlice'; +import { CREATE_DASHBOARD, UPDATE_DASHBOARD } from '@/shared/backend-bridge/apps/dashboards'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; const CUSTOMIZATION_ITEMS = [ @@ -65,13 +65,13 @@ const Sidebar: React.FC = ({ showUpdateDot }) => { const handleStartRename = (id: string, name: string) => { setRenamingId(id); setRenameValue(name); }; const handleRenameSubmit = (id: string) => { const t = renameValue.trim(); - if (t && t !== dashboardItems[id]?.name) dispatch(renameDashboard({ id, name: t })); + if (t && t !== dashboardItems[id]?.name) dispatch(UPDATE_DASHBOARD({ dashboardId: id, name: t })); setRenamingId(null); }; const handleCreateDash = async (e: React.MouseEvent) => { e.stopPropagation(); - const r = await dispatch(createDashboard('Untitled Dashboard')); - if (createDashboard.fulfilled.match(r)) navigate(`/dashboard/${r.payload.id}`); + const r = await dispatch(CREATE_DASHBOARD('Untitled Dashboard')); + if (CREATE_DASHBOARD.fulfilled.match(r)) navigate(`/dashboard/${r.payload.id}`); }; const handleAppsClick = () => { if (isAppsRoute && location.pathname === '/apps') setAppsExpanded((p) => !p); diff --git a/frontend/src/app/components/Layout/hooks/useUrlInterception.ts b/frontend/src/app/components/Layout/hooks/useUrlInterception.ts index ece35ec0..696cdc28 100644 --- a/frontend/src/app/components/Layout/hooks/useUrlInterception.ts +++ b/frontend/src/app/components/Layout/hooks/useUrlInterception.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useAppDispatch } from '@/shared/hooks'; -import { createDashboard } from '@/shared/state/dashboardsSlice'; +import { CREATE_DASHBOARD } from '@/shared/backend-bridge/apps/dashboards'; import { addBrowserCard, addBrowserTab } from '@/shared/state/dashboardLayoutSlice'; import { setPendingBrowserUrl } from '@/shared/state/tempStateSlice'; import { findBrowserByWebContentsId } from '@/shared/browserRegistry'; @@ -30,8 +30,8 @@ export function useUrlInterception(dashboardList: { id: string }[]) { if (targetId) { navigate(`/dashboard/${targetId}`); } else { - dispatch(createDashboard('Untitled Dashboard')).then((result: any) => { - if (createDashboard.fulfilled.match(result)) { + dispatch(CREATE_DASHBOARD('Untitled Dashboard')).then((result: any) => { + if (CREATE_DASHBOARD.fulfilled.match(result)) { navigate(`/dashboard/${result.payload.id}`); } }); diff --git a/frontend/src/app/pages/Dashboard/hooks/useDashboardThumbnail.ts b/frontend/src/app/pages/Dashboard/hooks/useDashboardThumbnail.ts index c97c9a56..2778e211 100644 --- a/frontend/src/app/pages/Dashboard/hooks/useDashboardThumbnail.ts +++ b/frontend/src/app/pages/Dashboard/hooks/useDashboardThumbnail.ts @@ -1,7 +1,7 @@ import { useEffect, useCallback, useRef } from 'react'; import type { RefObject } from 'react'; import { store } from '@/shared/state/store'; -import { updateDashboardThumbnail } from '@/shared/state/dashboardsSlice'; +import { UPDATE_DASHBOARD } from '@/shared/backend-bridge/apps/dashboards'; import { captureDashboardThumbnail } from '../captureDashboardThumbnail'; export function useDashboardThumbnail( @@ -45,7 +45,7 @@ export function useDashboardThumbnail( return () => { const thumbnail = pendingThumbnailRef.current; if (thumbnail) { - store.dispatch(updateDashboardThumbnail({ id: exitingId, thumbnail })); + store.dispatch(UPDATE_DASHBOARD({ dashboardId: exitingId, thumbnail })); pendingThumbnailRef.current = null; } }; diff --git a/frontend/src/app/pages/Dashboard/hooks/useToolbarActions.ts b/frontend/src/app/pages/Dashboard/hooks/useToolbarActions.ts index d7ab9c24..d23d6a53 100644 --- a/frontend/src/app/pages/Dashboard/hooks/useToolbarActions.ts +++ b/frontend/src/app/pages/Dashboard/hooks/useToolbarActions.ts @@ -22,7 +22,7 @@ import { ViewCardPosition, BrowserCardPosition } from '@/shared/state/dashboardLayoutSlice'; -import { generateDashboardName } from '@/shared/state/dashboardsSlice'; +import { GENERATE_DASHBOARD_NAME } from '@/shared/backend-bridge/apps/dashboards'; import type { ContextPath } from '@/app/components/DirectoryBrowser'; import type { CanvasActions } from '../useCanvasControls'; @@ -150,7 +150,7 @@ export function useToolbarActions(deps: ToolbarDeps) { currentDash && (currentDash.auto_named || currentDash.name === 'Untitled Dashboard'); if (NAME_GEN_TRIGGERS.includes(agentCount) && canAutoName) { - dispatch(generateDashboardName(dashboardId)); + dispatch(GENERATE_DASHBOARD_NAME(dashboardId)); } } } else { diff --git a/frontend/src/app/pages/DashboardSelection/DashboardSelection.tsx b/frontend/src/app/pages/DashboardSelection/DashboardSelection.tsx index e8187f9a..fc6dcc82 100644 --- a/frontend/src/app/pages/DashboardSelection/DashboardSelection.tsx +++ b/frontend/src/app/pages/DashboardSelection/DashboardSelection.tsx @@ -15,13 +15,13 @@ import EditIcon from '@mui/icons-material/Edit'; import SearchIcon from '@mui/icons-material/Search'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { - fetchDashboards, - createDashboard, - deleteDashboard, - duplicateDashboard, - renameDashboard, - Dashboard, -} from '@/shared/state/dashboardsSlice'; + LIST_DASHBOARDS, + CREATE_DASHBOARD, + DELETE_DASHBOARD, + DUPLICATE_DASHBOARD, + UPDATE_DASHBOARD, +} from '@/shared/backend-bridge/apps/dashboards'; +import type { Dashboard } from '@/shared/state/dashboardsSlice'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import DashboardCard from './DashboardCard'; @@ -39,7 +39,7 @@ const DashboardSelection: React.FC = () => { const [renameValue, setRenameValue] = useState(''); useEffect(() => { - dispatch(fetchDashboards()); + dispatch(LIST_DASHBOARDS()); }, [dispatch]); const dashboards = useMemo(() => { @@ -52,8 +52,8 @@ const DashboardSelection: React.FC = () => { }, [items, search]); const handleCreate = async () => { - const result = await dispatch(createDashboard('Untitled Dashboard')); - if (createDashboard.fulfilled.match(result)) { + const result = await dispatch(CREATE_DASHBOARD('Untitled Dashboard')); + if (CREATE_DASHBOARD.fulfilled.match(result)) { navigate(`/dashboard/${result.payload.id}`); } }; @@ -70,12 +70,12 @@ const DashboardSelection: React.FC = () => { }; const handleDelete = () => { - if (menuDashboard) dispatch(deleteDashboard(menuDashboard.id)); + if (menuDashboard) dispatch(DELETE_DASHBOARD(menuDashboard.id)); handleCloseMenu(); }; const handleDuplicate = () => { - if (menuDashboard) dispatch(duplicateDashboard(menuDashboard.id)); + if (menuDashboard) dispatch(DUPLICATE_DASHBOARD(menuDashboard.id)); handleCloseMenu(); }; @@ -93,7 +93,7 @@ const DashboardSelection: React.FC = () => { const handleRenameSubmit = (id: string) => { const trimmed = renameValue.trim(); if (trimmed && trimmed !== items[id]?.name) { - dispatch(renameDashboard({ id, name: trimmed })); + dispatch(UPDATE_DASHBOARD({ dashboardId: id, name: trimmed })); } setRenamingId(null); }; diff --git a/frontend/src/shared/backend-bridge/apps/dashboards.ts b/frontend/src/shared/backend-bridge/apps/dashboards.ts index bd2a313c..52496a0a 100644 --- a/frontend/src/shared/backend-bridge/apps/dashboards.ts +++ b/frontend/src/shared/backend-bridge/apps/dashboards.ts @@ -3,19 +3,28 @@ import { API_BASE } from '@/shared/backend-bridge/base_routes'; const DASHBOARDS_API: string = `${API_BASE}/dashboards`; +export interface Dashboard { + id: string; + name: string; + auto_named: boolean; + created_at: string; + updated_at: string; + thumbnail?: string | null; +} + // --------------------------------------------------------------------------- // Dashboard CRUD // --------------------------------------------------------------------------- const list_dashboards_endpoint: string = `${DASHBOARDS_API}/list`; -async function list_dashboards_function(): Promise[]> { +async function list_dashboards_function(): Promise { const res = await fetch(list_dashboards_endpoint, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); const data = await res.json(); - return data.dashboards as Record[]; + return data.dashboards as Dashboard[]; } export const LIST_DASHBOARDS = createAsyncThunk( list_dashboards_endpoint, @@ -24,14 +33,14 @@ export const LIST_DASHBOARDS = createAsyncThunk( const create_dashboard_endpoint: string = `${DASHBOARDS_API}/create`; -async function create_dashboard_function(name: string = 'Untitled Dashboard'): Promise> { +async function create_dashboard_function(name: string = 'Untitled Dashboard'): Promise { const res = await fetch(create_dashboard_endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); const data = await res.json(); - return data as Record; + return data as Dashboard; } export const CREATE_DASHBOARD = createAsyncThunk( create_dashboard_endpoint, @@ -40,13 +49,13 @@ export const CREATE_DASHBOARD = createAsyncThunk( const generate_dashboard_name_endpoint: string = `${DASHBOARDS_API}/generate-name`; -async function generate_dashboard_name_function(dashboardId: string): Promise<{ name: string; auto_named: boolean }> { +async function generate_dashboard_name_function(dashboardId: string): Promise<{ id: string; name: string; auto_named: boolean }> { const res = await fetch(`${DASHBOARDS_API}/${dashboardId}/generate-name`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); const data = await res.json(); - return data as { name: string; auto_named: boolean }; + return { id: dashboardId, name: data.name as string, auto_named: data.auto_named as boolean }; } export const GENERATE_DASHBOARD_NAME = createAsyncThunk( generate_dashboard_name_endpoint, @@ -55,13 +64,13 @@ export const GENERATE_DASHBOARD_NAME = createAsyncThunk( const get_dashboard_endpoint: string = `${DASHBOARDS_API}/get`; -async function get_dashboard_function(dashboardId: string): Promise> { +async function get_dashboard_function(dashboardId: string): Promise { const res = await fetch(`${DASHBOARDS_API}/${dashboardId}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); const data = await res.json(); - return data as Record; + return data as Dashboard; } export const GET_DASHBOARD = createAsyncThunk( get_dashboard_endpoint, @@ -75,15 +84,16 @@ async function update_dashboard_function(args: { name?: string; layout?: Record; thumbnail?: string; -}): Promise> { +}): Promise { const { dashboardId, ...updates } = args; const res = await fetch(`${DASHBOARDS_API}/${dashboardId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); + if (!res.ok) throw new Error(`Dashboard update failed: ${res.status}`); const data = await res.json(); - return data as Record; + return data as Dashboard; } export const UPDATE_DASHBOARD = createAsyncThunk( update_dashboard_endpoint, @@ -92,13 +102,13 @@ export const UPDATE_DASHBOARD = createAsyncThunk( const delete_dashboard_endpoint: string = `${DASHBOARDS_API}/delete`; -async function delete_dashboard_function(dashboardId: string): Promise<{ ok: boolean }> { +async function delete_dashboard_function(dashboardId: string): Promise { const res = await fetch(`${DASHBOARDS_API}/${dashboardId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }); - const data = await res.json(); - return data as { ok: boolean }; + await res.json(); + return dashboardId; } export const DELETE_DASHBOARD = createAsyncThunk( delete_dashboard_endpoint, @@ -107,13 +117,13 @@ export const DELETE_DASHBOARD = createAsyncThunk( const duplicate_dashboard_endpoint: string = `${DASHBOARDS_API}/duplicate`; -async function duplicate_dashboard_function(dashboardId: string): Promise> { +async function duplicate_dashboard_function(dashboardId: string): Promise { const res = await fetch(`${DASHBOARDS_API}/${dashboardId}/duplicate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); const data = await res.json(); - return data as Record; + return data as Dashboard; } export const DUPLICATE_DASHBOARD = createAsyncThunk( duplicate_dashboard_endpoint, diff --git a/frontend/src/shared/state/dashboardsSlice.ts b/frontend/src/shared/state/dashboardsSlice.ts index 6bb7b957..c0f7fd99 100644 --- a/frontend/src/shared/state/dashboardsSlice.ts +++ b/frontend/src/shared/state/dashboardsSlice.ts @@ -1,16 +1,15 @@ -import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; -import { API_BASE } from '@/shared/config'; +import { createSlice } from '@reduxjs/toolkit'; +import { + LIST_DASHBOARDS, + CREATE_DASHBOARD, + UPDATE_DASHBOARD, + DELETE_DASHBOARD, + DUPLICATE_DASHBOARD, + GENERATE_DASHBOARD_NAME, +} from '@/shared/backend-bridge/apps/dashboards'; +import type { Dashboard } from '@/shared/backend-bridge/apps/dashboards'; -const DASHBOARDS_API = `${API_BASE}/dashboards`; - -export interface Dashboard { - id: string; - name: string; - auto_named: boolean; - created_at: string; - updated_at: string; - thumbnail?: string | null; -} +export type { Dashboard }; interface DashboardsState { items: Record; @@ -22,87 +21,16 @@ const initialState: DashboardsState = { loading: false, }; -export const fetchDashboards = createAsyncThunk('dashboards/fetchAll', async () => { - const res = await fetch(`${DASHBOARDS_API}/list`); - const data = await res.json(); - return data.dashboards as Dashboard[]; -}); - -export const createDashboard = createAsyncThunk( - 'dashboards/create', - async (name: string) => { - const res = await fetch(`${DASHBOARDS_API}/create`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name }), - }); - return (await res.json()) as Dashboard; - }, -); - -export const renameDashboard = createAsyncThunk( - 'dashboards/rename', - async ({ id, name }: { id: string; name: string }) => { - const res = await fetch(`${DASHBOARDS_API}/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name }), - }); - return (await res.json()) as Dashboard; - }, -); - -export const deleteDashboard = createAsyncThunk( - 'dashboards/delete', - async (id: string) => { - await fetch(`${DASHBOARDS_API}/${id}`, { method: 'DELETE' }); - return id; - }, -); - -export const duplicateDashboard = createAsyncThunk( - 'dashboards/duplicate', - async (id: string) => { - const res = await fetch(`${DASHBOARDS_API}/${id}/duplicate`, { method: 'POST' }); - return (await res.json()) as Dashboard; - }, -); - -export const updateDashboardThumbnail = createAsyncThunk( - 'dashboards/updateThumbnail', - async ({ id, thumbnail }: { id: string; thumbnail: string }) => { - const res = await fetch(`${DASHBOARDS_API}/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ thumbnail }), - }); - if (!res.ok) throw new Error(`Thumbnail update failed: ${res.status}`); - const data = await res.json(); - return { id, thumbnail: data.thumbnail as string | null, updated_at: data.updated_at as string }; - }, -); - -export const generateDashboardName = createAsyncThunk( - 'dashboards/generateName', - async (dashboardId: string) => { - const res = await fetch(`${DASHBOARDS_API}/${dashboardId}/generate-name`, { - method: 'POST', - }); - const data = await res.json(); - return { id: dashboardId, name: data.name as string, auto_named: data.auto_named as boolean }; - }, -); - const dashboardsSlice = createSlice({ name: 'dashboards', initialState, reducers: {}, extraReducers: (builder) => { builder - .addCase(fetchDashboards.pending, (state) => { + .addCase(LIST_DASHBOARDS.pending, (state) => { state.loading = true; }) - .addCase(fetchDashboards.fulfilled, (state, action) => { + .addCase(LIST_DASHBOARDS.fulfilled, (state, action) => { state.loading = false; const items: Record = {}; for (const d of action.payload) { @@ -110,42 +38,30 @@ const dashboardsSlice = createSlice({ } state.items = items; }) - .addCase(fetchDashboards.rejected, (state) => { + .addCase(LIST_DASHBOARDS.rejected, (state) => { state.loading = false; }) - .addCase(createDashboard.fulfilled, (state, action) => { + .addCase(CREATE_DASHBOARD.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; }) - .addCase(renameDashboard.fulfilled, (state, action) => { + .addCase(UPDATE_DASHBOARD.fulfilled, (state, action) => { const d = action.payload; if (state.items[d.id]) { - state.items[d.id] = { - ...state.items[d.id], - name: d.name, - auto_named: d.auto_named ?? false, - updated_at: d.updated_at, - }; + state.items[d.id] = { ...state.items[d.id], ...d }; } }) - .addCase(deleteDashboard.fulfilled, (state, action) => { + .addCase(DELETE_DASHBOARD.fulfilled, (state, action) => { delete state.items[action.payload]; }) - .addCase(duplicateDashboard.fulfilled, (state, action) => { + .addCase(DUPLICATE_DASHBOARD.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; }) - .addCase(generateDashboardName.fulfilled, (state, action) => { + .addCase(GENERATE_DASHBOARD_NAME.fulfilled, (state, action) => { const { id, name, auto_named } = action.payload; if (state.items[id]) { state.items[id].name = name; state.items[id].auto_named = auto_named; } - }) - .addCase(updateDashboardThumbnail.fulfilled, (state, action) => { - const { id, thumbnail, updated_at } = action.payload; - if (state.items[id]) { - state.items[id].thumbnail = thumbnail; - state.items[id].updated_at = updated_at; - } }); }, });