[hAIk]: refactor: wire dashboardsSlice through backend-bridge, remove inline fetch calls from slice and update all 6 consumer files

This commit is contained in:
haikdc
2026-04-18 04:30:33 -07:00
parent 5c47a12181
commit 650c42077a
8 changed files with 71 additions and 145 deletions
@@ -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]);
@@ -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<SidebarProps> = ({ 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);
@@ -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}`);
}
});
@@ -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;
}
};
@@ -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 {
@@ -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);
};
@@ -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<Record<string, unknown>[]> {
async function list_dashboards_function(): Promise<Dashboard[]> {
const res = await fetch(list_dashboards_endpoint, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
return data.dashboards as Record<string, unknown>[];
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<Record<string, unknown>> {
async function create_dashboard_function(name: string = 'Untitled Dashboard'): Promise<Dashboard> {
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<string, unknown>;
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<Record<string, unknown>> {
async function get_dashboard_function(dashboardId: string): Promise<Dashboard> {
const res = await fetch(`${DASHBOARDS_API}/${dashboardId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
return data as Record<string, unknown>;
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<string, unknown>;
thumbnail?: string;
}): Promise<Record<string, unknown>> {
}): Promise<Dashboard> {
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<string, unknown>;
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<string> {
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<Record<string, unknown>> {
async function duplicate_dashboard_function(dashboardId: string): Promise<Dashboard> {
const res = await fetch(`${DASHBOARDS_API}/${dashboardId}/duplicate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
return data as Record<string, unknown>;
return data as Dashboard;
}
export const DUPLICATE_DASHBOARD = createAsyncThunk(
duplicate_dashboard_endpoint,
+20 -104
View File
@@ -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<string, Dashboard>;
@@ -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<string, Dashboard> = {};
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;
}
});
},
});