mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): improve mobile layout for settings and analytics (#23460)
This commit is contained in:
@@ -36,7 +36,7 @@ const AgentAnalyticsPage: FC<AgentAnalyticsPageProps> = ({ now }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AgentPageHeader />
|
||||
<AgentPageHeader mobileBack={{ to: "/agents", label: "Agents" }} />
|
||||
<AgentAnalyticsPageView
|
||||
summary={summaryQuery.data}
|
||||
isLoading={summaryQuery.isLoading}
|
||||
|
||||
@@ -10,7 +10,11 @@ const AgentSettingsPage: FC = () => {
|
||||
const isAgentsAdmin = permissions.editDeploymentConfig;
|
||||
return (
|
||||
<>
|
||||
<AgentPageHeader />
|
||||
<AgentPageHeader
|
||||
mobileBack={
|
||||
section ? { to: "/agents/settings", label: "Settings" } : undefined
|
||||
}
|
||||
/>
|
||||
<AgentSettingsPageView
|
||||
activeSection={section ?? "behavior"}
|
||||
canManageChatModelConfigs={isAgentsAdmin}
|
||||
|
||||
@@ -92,6 +92,14 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
||||
const location = useLocation();
|
||||
const sidebarView = sidebarViewFromPath(location.pathname);
|
||||
|
||||
// Mobile can't fit the sidebar nav and content side by side,
|
||||
// so we show one or the other depending on the route depth.
|
||||
const isSettingsIndex =
|
||||
sidebarView.panel === "settings" && !sidebarView.section;
|
||||
const isSettingsDetail =
|
||||
sidebarView.panel === "settings" && Boolean(sidebarView.section);
|
||||
const isAnalytics = sidebarView.panel === "analytics";
|
||||
|
||||
// The sidebar expects plain string error messages, but the outlet
|
||||
// context now carries structured ChatDetailError objects.
|
||||
const sidebarChatErrorReasons = Object.fromEntries(
|
||||
@@ -121,7 +129,9 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
||||
"md:h-full md:w-[320px] md:min-h-0 md:border-b-0",
|
||||
agentId
|
||||
? "hidden md:block shrink-0 h-[42dvh] min-h-[240px] border-b border-border-default"
|
||||
: "order-2 md:order-none flex-1 min-h-0 border-t border-border-default md:flex-none md:border-t-0",
|
||||
: isSettingsDetail || isAnalytics
|
||||
? "hidden md:block shrink-0"
|
||||
: "order-2 md:order-none flex-1 min-h-0 border-t border-border-default md:flex-none md:border-t-0",
|
||||
isSidebarCollapsed && "md:hidden",
|
||||
)}
|
||||
>
|
||||
@@ -150,11 +160,12 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
||||
isAdmin={isAgentsAdmin}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-0 min-w-0 flex-1 flex-col bg-surface-primary",
|
||||
"min-h-0 min-w-0 flex-1 flex-col bg-surface-primary",
|
||||
isSettingsIndex ? "hidden md:flex" : "flex",
|
||||
!agentId &&
|
||||
!isSettingsDetail &&
|
||||
sidebarView.panel === "chats" &&
|
||||
"order-1 md:order-none flex-none md:flex-1",
|
||||
)}
|
||||
|
||||
@@ -275,7 +275,7 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</ScrollAnchoredContainer>
|
||||
<div className="shrink-0 overflow-y-auto px-4 [scrollbar-gutter:stable] [scrollbar-width:thin]">
|
||||
<div className="shrink-0 overflow-y-auto px-4 pb-4 md:pb-0 [scrollbar-gutter:stable] [scrollbar-width:thin]">
|
||||
<AgentDetailInput
|
||||
store={store}
|
||||
compressionThreshold={compressionThreshold}
|
||||
@@ -414,7 +414,7 @@ export const AgentDetailLoadingView: FC<AgentDetailLoadingViewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 overflow-y-auto px-4 [scrollbar-gutter:stable] [scrollbar-width:thin]">
|
||||
<div className="shrink-0 overflow-y-auto px-4 pb-4 md:pb-0 [scrollbar-gutter:stable] [scrollbar-width:thin]">
|
||||
<AgentChatInput
|
||||
onSend={() => {}}
|
||||
initialValue=""
|
||||
|
||||
@@ -1,31 +1,56 @@
|
||||
import { Button } from "components/Button/Button";
|
||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import { CoderIcon } from "components/Icons/CoderIcon";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
import {
|
||||
BarChart3Icon,
|
||||
ChevronLeftIcon,
|
||||
PanelLeftIcon,
|
||||
SettingsIcon,
|
||||
} from "lucide-react";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { NavLink, useOutletContext } from "react-router";
|
||||
import { Link, NavLink, useLocation, useOutletContext } from "react-router";
|
||||
import { cn } from "utils/cn";
|
||||
import type { AgentsOutletContext } from "../AgentsPageView";
|
||||
import { sidebarViewFromPath } from "./Sidebar/AgentsSidebar";
|
||||
|
||||
interface AgentPageHeaderProps {
|
||||
children?: ReactNode;
|
||||
/** When set, shows a back link on mobile instead of the logo
|
||||
* and hides the settings/analytics nav buttons. */
|
||||
mobileBack?: { to: string; label: string };
|
||||
}
|
||||
|
||||
export const AgentPageHeader: FC<AgentPageHeaderProps> = ({ children }) => {
|
||||
export const AgentPageHeader: FC<AgentPageHeaderProps> = ({
|
||||
children,
|
||||
mobileBack,
|
||||
}) => {
|
||||
const { isSidebarCollapsed, onExpandSidebar } =
|
||||
useOutletContext<AgentsOutletContext>();
|
||||
const { appearance } = useDashboard();
|
||||
const logoUrl = appearance.logo_url;
|
||||
const location = useLocation();
|
||||
const sidebarView = sidebarViewFromPath(location.pathname);
|
||||
|
||||
return (
|
||||
<div className="flex shrink-0 items-center gap-2 px-4 py-0.5">
|
||||
<NavLink to="/workspaces" className="inline-flex shrink-0 md:hidden">
|
||||
{logoUrl ? (
|
||||
<ExternalImage className="h-6" src={logoUrl} alt="Logo" />
|
||||
) : (
|
||||
<CoderIcon className="h-6 w-6 fill-content-primary" />
|
||||
)}
|
||||
</NavLink>
|
||||
<div className="flex shrink-0 items-center gap-2 px-4 pt-3 pb-0.5 md:py-0.5">
|
||||
{mobileBack ? (
|
||||
<Link
|
||||
to={mobileBack.to}
|
||||
className="inline-flex shrink-0 items-center gap-1 text-sm text-content-secondary no-underline hover:text-content-primary md:hidden"
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
{mobileBack.label}
|
||||
</Link>
|
||||
) : (
|
||||
<NavLink to="/workspaces" className="inline-flex shrink-0 md:hidden">
|
||||
{logoUrl ? (
|
||||
<ExternalImage className="h-6" src={logoUrl} alt="Logo" />
|
||||
) : (
|
||||
<CoderIcon className="h-6 w-6 fill-content-primary" />
|
||||
)}
|
||||
</NavLink>
|
||||
)}
|
||||
{isSidebarCollapsed && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -38,6 +63,40 @@ export const AgentPageHeader: FC<AgentPageHeaderProps> = ({ children }) => {
|
||||
</Button>
|
||||
)}
|
||||
<div className="min-w-0 flex-1" />
|
||||
{/* Mobile-only nav buttons mirroring the sidebar toolbar
|
||||
* which is hidden below the md breakpoint. */}
|
||||
{!mobileBack && (
|
||||
<div className="flex items-center gap-0.5 md:hidden">
|
||||
<Button
|
||||
asChild
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
aria-label="Settings"
|
||||
className={cn(
|
||||
"h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary",
|
||||
sidebarView.panel === "settings" && "text-content-primary",
|
||||
)}
|
||||
>
|
||||
<Link to="/agents/settings" state={{ from: location.pathname }}>
|
||||
<SettingsIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
aria-label="Analytics"
|
||||
className={cn(
|
||||
"h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary",
|
||||
sidebarView.panel === "analytics" && "text-content-primary",
|
||||
)}
|
||||
>
|
||||
<Link to="/agents/analytics">
|
||||
<BarChart3Icon />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{children && <div className="flex items-center gap-2">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -74,7 +74,7 @@ import { UsageIndicator } from "../UsageIndicator";
|
||||
|
||||
type SidebarView =
|
||||
| { panel: "chats" }
|
||||
| { panel: "settings"; section: string }
|
||||
| { panel: "settings"; section: string | undefined }
|
||||
| { panel: "analytics" };
|
||||
|
||||
/**
|
||||
@@ -86,7 +86,7 @@ export function sidebarViewFromPath(pathname: string): SidebarView {
|
||||
}
|
||||
const settingsMatch = pathname.match(/^\/agents\/settings(?:\/([^/]+))?/);
|
||||
if (settingsMatch) {
|
||||
return { panel: "settings", section: settingsMatch[1] || "behavior" };
|
||||
return { panel: "settings", section: settingsMatch[1] };
|
||||
}
|
||||
return { panel: "chats" };
|
||||
}
|
||||
@@ -689,7 +689,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
{/* ── Panel 1: Chats ── */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex flex-col transition-transform duration-200 ease-in-out",
|
||||
"absolute inset-0 flex flex-col md:transition-transform md:duration-200 md:ease-in-out",
|
||||
sidebarView.panel === "settings" && "-translate-x-full",
|
||||
)}
|
||||
aria-hidden={sidebarView.panel === "settings"}
|
||||
@@ -923,21 +923,24 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
{/* ── Panel 2: Sub-navigation (Settings) ── */}{" "}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex flex-col transition-transform duration-200 ease-in-out",
|
||||
"absolute inset-0 flex flex-col md:transition-transform md:duration-200 md:ease-in-out",
|
||||
sidebarView.panel !== "settings" && "translate-x-full",
|
||||
)}
|
||||
aria-hidden={sidebarView.panel !== "settings"}
|
||||
inert={sidebarView.panel !== "settings" ? true : undefined}
|
||||
>
|
||||
{/* Back header */}
|
||||
<div className="hidden border-b border-border-default px-2 py-2 md:block">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="border-b border-border-default px-2 pb-2 pt-3 md:py-2">
|
||||
<div className="relative flex items-center">
|
||||
<span className="pointer-events-none absolute inset-0 flex items-center justify-center text-sm font-medium text-content-primary">
|
||||
{subNavTitle}
|
||||
</span>
|
||||
<Button
|
||||
asChild
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
aria-label="Back to chats"
|
||||
className="h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary"
|
||||
className="relative z-10 h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary"
|
||||
>
|
||||
<Link
|
||||
to={(location.state as { from?: string })?.from || "/agents"}
|
||||
@@ -945,22 +948,18 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
<ChevronLeftIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<span className="text-sm font-medium text-content-primary">
|
||||
{subNavTitle}
|
||||
</span>
|
||||
<div className="flex items-center gap-0.5 -mr-1.5">
|
||||
{onCollapse && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
onClick={onCollapse}
|
||||
aria-label="Collapse sidebar"
|
||||
className="h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary"
|
||||
>
|
||||
<PanelLeftCloseIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
{onCollapse && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
onClick={onCollapse}
|
||||
aria-label="Collapse sidebar"
|
||||
className="relative z-10 hidden h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary md:inline-flex"
|
||||
>
|
||||
<PanelLeftCloseIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Sub-navigation items */}
|
||||
@@ -969,7 +968,9 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
<SettingsNavItem
|
||||
icon={UserIcon}
|
||||
label="Behavior"
|
||||
active={sidebarView.section === "behavior"}
|
||||
active={
|
||||
!sidebarView.section || sidebarView.section === "behavior"
|
||||
}
|
||||
to="/agents/settings/behavior"
|
||||
state={location.state}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user