feat: settings data loading per page (#51)

This commit is contained in:
Carlos Florêncio
2026-01-20 00:47:26 +00:00
committed by GitHub
parent 0ac6a87132
commit 3286fd54c8
7 changed files with 303 additions and 297 deletions
+37 -2
View File
@@ -1,5 +1,40 @@
import { EditorsSettings } from '@/components/settings/editors-settings';
import { StateProvider } from '@/components/state-provider';
import { fetchUserSettings, listEditors } from '@/lib/http';
export default function GeneralEditorsPage() {
return <EditorsSettings />;
export default async function GeneralEditorsPage() {
let initialState = {};
try {
const [editorsResponse, settingsResponse] = await Promise.all([
listEditors({ cache: 'no-store' }),
fetchUserSettings({ cache: 'no-store' }),
]);
const settings = settingsResponse.settings;
initialState = {
editors: {
items: editorsResponse.editors ?? [],
loaded: true,
loading: false,
},
userSettings: settings
? {
workspaceId: settings.workspace_id ?? null,
boardId: settings.board_id ?? null,
repositoryIds: settings.repository_ids ?? [],
preferredShell: settings.preferred_shell ?? null,
shellOptions: settingsResponse.shell_options ?? [],
defaultEditorId: settings.default_editor_id ?? null,
loaded: true,
}
: undefined,
};
} catch {
initialState = {};
}
return (
<StateProvider initialState={initialState}>
<EditorsSettings />
</StateProvider>
);
}
@@ -1,5 +1,27 @@
import { NotificationsSettings } from '@/components/settings/notifications-settings';
import { StateProvider } from '@/components/state-provider';
import { listNotificationProviders } from '@/lib/http';
export default function GeneralNotificationsPage() {
return <NotificationsSettings />;
export default async function GeneralNotificationsPage() {
let initialState = {};
try {
const response = await listNotificationProviders({ cache: 'no-store' });
initialState = {
notificationProviders: {
items: response.providers ?? [],
events: response.events ?? [],
appriseAvailable: response.apprise_available ?? false,
loaded: true,
loading: false,
},
};
} catch {
initialState = {};
}
return (
<StateProvider initialState={initialState}>
<NotificationsSettings />
</StateProvider>
);
}
+29 -2
View File
@@ -1,5 +1,32 @@
import { GeneralSettings } from '@/components/settings/general-settings';
import { StateProvider } from '@/components/state-provider';
import { fetchUserSettings } from '@/lib/http';
export default function GeneralSettingsPage() {
return <GeneralSettings />;
export default async function GeneralSettingsPage() {
let initialState = {};
try {
const response = await fetchUserSettings({ cache: 'no-store' });
const settings = response.settings;
initialState = {
userSettings: settings
? {
workspaceId: settings.workspace_id ?? null,
boardId: settings.board_id ?? null,
repositoryIds: settings.repository_ids ?? [],
preferredShell: settings.preferred_shell ?? null,
shellOptions: response.shell_options ?? [],
defaultEditorId: settings.default_editor_id ?? null,
loaded: true,
}
: undefined,
};
} catch {
initialState = {};
}
return (
<StateProvider initialState={initialState}>
<GeneralSettings />
</StateProvider>
);
}
-43
View File
@@ -6,10 +6,6 @@ import {
listEnvironments,
listExecutors,
listWorkspaces,
fetchUserSettings,
listPrompts,
listNotificationProviders,
listEditors,
} from '@/lib/http';
export default function SettingsLayout({
@@ -31,26 +27,13 @@ async function SettingsLayoutServer({ children }: { children: React.ReactNode })
environments,
agents,
discovery,
userSettings,
promptsResponse,
notificationProviders,
editorsResponse,
] = await Promise.all([
listWorkspaces({ cache: 'no-store' }),
listExecutors({ cache: 'no-store' }),
listEnvironments({ cache: 'no-store' }),
listAgents({ cache: 'no-store' }),
listAgentDiscovery({ cache: 'no-store' }),
fetchUserSettings({ cache: 'no-store' }).catch(() => null),
listPrompts({ cache: 'no-store' }).catch(() => ({ prompts: [] })),
listNotificationProviders({ cache: 'no-store' }).catch(() => ({
providers: [],
apprise_available: false,
events: [],
})),
listEditors({ cache: 'no-store' }).catch(() => ({ editors: [] })),
]);
const settings = userSettings?.settings;
initialState = {
workspaces: {
items: workspaces.workspaces.map((workspace) => ({
@@ -88,32 +71,6 @@ async function SettingsLayoutServer({ children }: { children: React.ReactNode })
environmentsLoaded: true,
agentsLoaded: true,
},
prompts: {
items: promptsResponse.prompts ?? [],
loaded: true,
loading: false,
},
editors: {
items: editorsResponse.editors ?? [],
loaded: true,
loading: false,
},
notificationProviders: {
items: notificationProviders.providers ?? [],
events: notificationProviders.events ?? [],
appriseAvailable: notificationProviders.apprise_available ?? false,
loaded: true,
loading: false,
},
userSettings: {
workspaceId: settings?.workspace_id ?? null,
boardId: settings?.board_id ?? null,
repositoryIds: settings?.repository_ids ?? [],
preferredShell: settings?.preferred_shell ?? null,
shellOptions: userSettings?.shell_options ?? [],
defaultEditorId: settings?.default_editor_id ?? null,
loaded: Boolean(settings),
},
};
} catch {
initialState = {};
+21 -1
View File
@@ -1,5 +1,25 @@
import { PromptsSettings } from '@/components/settings/prompts-settings';
import { StateProvider } from '@/components/state-provider';
import { listPrompts } from '@/lib/http';
export default async function PromptsSettingsPage() {
return <PromptsSettings />;
let initialState = {};
try {
const response = await listPrompts({ cache: 'no-store' });
initialState = {
prompts: {
items: response.prompts ?? [],
loaded: true,
loading: false,
},
};
} catch {
initialState = {};
}
return (
<StateProvider initialState={initialState}>
<PromptsSettings />
</StateProvider>
);
}
@@ -1,6 +1,6 @@
'use client';
import { useMemo, useState, type FormEvent } from 'react';
import { useMemo, useReducer, useState, useSyncExternalStore, type FormEvent } from 'react';
import { IconBell, IconRefresh } from '@tabler/icons-react';
import { Button } from '@kandev/ui/button';
import { Checkbox } from '@kandev/ui/checkbox';
@@ -105,14 +105,17 @@ export function NotificationsSettings() {
const [appriseFormMode, setAppriseFormMode] = useState<AppriseFormMode>('create');
const [activeAppriseId, setActiveAppriseId] = useState<string | null>(null);
const [pendingDeletes, setPendingDeletes] = useState<Set<string>>(new Set());
const [notificationPermission, setNotificationPermission] = useState<
NotificationPermission | 'unsupported'
>(() => {
if (typeof Notification === 'undefined') {
return 'unsupported';
}
return Notification.permission;
});
const mounted = useSyncExternalStore(
() => () => undefined,
() => true,
() => false
);
const [, bumpPermission] = useReducer((value) => value + 1, 0);
const notificationPermission: NotificationPermission | 'unsupported' = !mounted
? 'default'
: typeof Notification === 'undefined'
? 'unsupported'
: Notification.permission;
const saveRequest = useRequest(async () => {
const updates: Array<Promise<NotificationProvider>> = [];
@@ -215,19 +218,17 @@ export function NotificationsSettings() {
const handleRequestPermission = async () => {
if (typeof Notification === 'undefined') {
setNotificationPermission('unsupported');
return;
}
const result = await Notification.requestPermission();
setNotificationPermission(result);
await Notification.requestPermission();
bumpPermission();
};
const handleRefreshPermission = () => {
if (typeof Notification === 'undefined') {
setNotificationPermission('unsupported');
return;
}
setNotificationPermission(Notification.permission);
bumpPermission();
};
const handleTestNotification = async () => {
@@ -237,7 +238,7 @@ export function NotificationsSettings() {
let permission = Notification.permission;
if (permission === 'default') {
permission = await Notification.requestPermission();
setNotificationPermission(permission);
bumpPermission();
}
if (permission !== 'granted') {
return;
@@ -28,6 +28,7 @@ import {
SidebarMenuAction,
SidebarHeader,
} from '@kandev/ui/sidebar';
import { ScrollArea } from '@kandev/ui/scroll-area';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@kandev/ui/collapsible';
import { useAppStore } from '@/components/state-provider';
import { useAvailableAgents } from '@/hooks/use-available-agents';
@@ -59,245 +60,187 @@ export function SettingsAppSidebar() {
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Settings</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarContent className="overflow-hidden">
<ScrollArea className="h-full" type="always">
<SidebarGroup>
<SidebarGroupLabel>Settings</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{/* General */}
<Collapsible defaultOpen className="group/collapsible">
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="General">
<Link href="/settings/general">
<IconSettings className="h-4 w-4" />
<span>General</span>
</Link>
</SidebarMenuButton>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<IconChevronRight className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === '/settings/general/notifications'}
>
<Link href="/settings/general/notifications">
<IconBell className="h-4 w-4" />
<span>Notifications</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === '/settings/general/editors'}
>
<Link href="/settings/general/editors">
<IconCode className="h-4 w-4" />
<span>Editors</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="General">
<Link href="/settings/general">
<IconSettings className="h-4 w-4" />
<span>General</span>
</Link>
</SidebarMenuButton>
<SidebarMenuSub className="ml-3 mt-1">
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === '/settings/general/notifications'}
>
<Link href="/settings/general/notifications">
<IconBell className="h-4 w-4" />
<span>Notifications</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === '/settings/general/editors'}
>
<Link href="/settings/general/editors">
<IconCode className="h-4 w-4" />
<span>Editors</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</SidebarMenuItem>
{/* Workspaces */}
<Collapsible defaultOpen className="group/collapsible">
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Workspaces">
<Link href="/settings/workspace">
<IconFolder className="h-4 w-4" />
<span>Workspaces</span>
</Link>
</SidebarMenuButton>
{workspaces.length > 0 && (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<IconChevronRight className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{workspaces.map((workspace) => {
const workspacePath = `/settings/workspace/${workspace.id}`;
const boardsPath = `${workspacePath}/boards`;
const repositoriesPath = `${workspacePath}/repositories`;
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Workspaces">
<Link href="/settings/workspace">
<IconFolder className="h-4 w-4" />
<span>Workspaces</span>
</Link>
</SidebarMenuButton>
{workspaces.length > 0 && (
<SidebarMenuSub className="ml-3 mt-1">
{workspaces.map((workspace) => {
const workspacePath = `/settings/workspace/${workspace.id}`;
const boardsPath = `${workspacePath}/boards`;
const repositoriesPath = `${workspacePath}/repositories`;
return (
<SidebarMenuSubItem key={workspace.id}>
<SidebarMenuSubButton
asChild
isActive={pathname === workspacePath}
>
<Link href={workspacePath}>
<span>{workspace.name}</span>
</Link>
</SidebarMenuSubButton>
<SidebarMenuSub className="ml-3">
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === repositoriesPath}
>
<Link href={repositoriesPath}>
<span>Repositories</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === boardsPath}
>
<Link href={boardsPath}>
<span>Boards</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
return (
<SidebarMenuSubItem key={workspace.id}>
<SidebarMenuSubButton
asChild
isActive={pathname === workspacePath}
>
<Link href={workspacePath}>
<span>{workspace.name}</span>
</Link>
</SidebarMenuSubButton>
<SidebarMenuSub className="ml-3">
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === repositoriesPath}
>
<Link href={repositoriesPath}>
<span>Repositories</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton
asChild
size="sm"
isActive={pathname === boardsPath}
>
<Link href={boardsPath}>
<span>Boards</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
)}
</SidebarMenuItem>
{/* Environments */}
<Collapsible defaultOpen className="group/collapsible">
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Environments">
<Link href="/settings/environments">
<IconServer className="h-4 w-4" />
<span>Environments</span>
</Link>
</SidebarMenuButton>
{environments.length > 0 && (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<IconChevronRight className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{environments.map((env) => (
<SidebarMenuSubItem key={env.id}>
<SidebarMenuSubButton
asChild
isActive={pathname === `/settings/environment/${env.id}`}
>
<Link href={`/settings/environment/${env.id}`}>
<span>{env.name}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Environments">
<Link href="/settings/environments">
<IconServer className="h-4 w-4" />
<span>Environments</span>
</Link>
</SidebarMenuButton>
{environments.length > 0 && (
<SidebarMenuSub className="ml-3 mt-1">
{environments.map((env) => (
<SidebarMenuSubItem key={env.id}>
<SidebarMenuSubButton
asChild
isActive={pathname === `/settings/environment/${env.id}`}
>
<Link href={`/settings/environment/${env.id}`}>
<span>{env.name}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
)}
</SidebarMenuItem>
{/* Executors */}
<Collapsible defaultOpen className="group/collapsible">
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Executors">
<Link href="/settings/executors">
<IconCpu className="h-4 w-4" />
<span>Executors</span>
</Link>
</SidebarMenuButton>
{executors.length > 0 && (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<IconChevronRight className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{executors.map((executor) => (
<SidebarMenuSubItem key={executor.id}>
<SidebarMenuSubButton
asChild
isActive={pathname === `/settings/executor/${executor.id}`}
>
<Link href={`/settings/executor/${executor.id}`}>
<span>{executor.name}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Executors">
<Link href="/settings/executors">
<IconCpu className="h-4 w-4" />
<span>Executors</span>
</Link>
</SidebarMenuButton>
{executors.length > 0 && (
<SidebarMenuSub className="ml-3 mt-1">
{executors.map((executor) => (
<SidebarMenuSubItem key={executor.id}>
<SidebarMenuSubButton
asChild
isActive={pathname === `/settings/executor/${executor.id}`}
>
<Link href={`/settings/executor/${executor.id}`}>
<span>{executor.name}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
)}
</SidebarMenuItem>
{/* Agents */}
<Collapsible defaultOpen className="group/collapsible">
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Agents">
<Link href="/settings/agents">
<IconRobot className="h-4 w-4" />
<span>Agents</span>
</Link>
</SidebarMenuButton>
{agents.length > 0 && (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<IconChevronRight className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{agents.flatMap((agent) =>
agent.profiles.map((profile) => {
const encodedAgent = encodeURIComponent(agent.name);
const profilePath = `/settings/agents/${encodedAgent}/profiles/${profile.id}`;
const agentLabel = profile.agent_display_name || agent.name;
return (
<SidebarMenuSubItem key={profile.id}>
<SidebarMenuSubButton
asChild
isActive={pathname === profilePath}
>
<Link href={profilePath}>
<span>{agentLabel} • {profile.name}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
})
)}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Agents">
<Link href="/settings/agents">
<IconRobot className="h-4 w-4" />
<span>Agents</span>
</Link>
</SidebarMenuButton>
{agents.length > 0 && (
<SidebarMenuSub className="ml-3 mt-1">
{agents.flatMap((agent) =>
agent.profiles.map((profile) => {
const encodedAgent = encodeURIComponent(agent.name);
const profilePath = `/settings/agents/${encodedAgent}/profiles/${profile.id}`;
const agentLabel = profile.agent_display_name || agent.name;
return (
<SidebarMenuSubItem key={profile.id}>
<SidebarMenuSubButton asChild isActive={pathname === profilePath}>
<Link href={profilePath}>
<span>
{agentLabel} • {profile.name}
</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
})
)}
</SidebarMenuSub>
)}
</SidebarMenuItem>
{/* Prompts */}
<SidebarMenuItem>
@@ -308,9 +251,10 @@ export function SettingsAppSidebar() {
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</ScrollArea>
</SidebarContent>
</Sidebar>
);