mirror of
https://github.com/coder/coder.git
synced 2026-09-23 22:20:22 +08:00
fix: sidebar and analytics UI tweaks (#23499)
<img width="684" height="540" alt="image" src="https://github.com/user-attachments/assets/ccd09873-4640-4a54-b3ca-f740dd50b38d" /> ## Changes - Move filter dropdown from top nav bar to inline with the first time group header (e.g. "Today") - Remove analytics icon from desktop sidebar nav bar - Change "View details" to "View usage" in the usage indicator dropdown - Fix green progress bar visibility in dark mode (`bg-surface-green` → `bg-content-success`) - Fix missing space before date in "Resets" text --- PR generated with Coder Agents
This commit is contained in:
@@ -486,11 +486,6 @@ export const WithErrorReasons: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
const openAnalyticsView = async (canvasElement: HTMLElement) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByRole("link", { name: "Analytics" }));
|
||||
};
|
||||
|
||||
const openSettingsView = async (canvasElement: HTMLElement) => {
|
||||
const canvas = within(canvasElement);
|
||||
const link = await waitFor(() =>
|
||||
@@ -503,9 +498,13 @@ export const OpensAnalyticsForAdmins: Story = {
|
||||
args: {
|
||||
isAgentsAdmin: true,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await openAnalyticsView(canvasElement);
|
||||
|
||||
parameters: {
|
||||
reactRouter: reactRouterParameters({
|
||||
location: { path: "/agents/analytics" },
|
||||
routing: agentsRouting,
|
||||
}),
|
||||
},
|
||||
play: async () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
@@ -522,10 +521,12 @@ export const OpensAnalyticsForNonAdmins: Story = {
|
||||
},
|
||||
parameters: {
|
||||
permissions: MockNoPermissions,
|
||||
reactRouter: reactRouterParameters({
|
||||
location: { path: "/agents/analytics" },
|
||||
routing: agentsRouting,
|
||||
}),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await openAnalyticsView(canvasElement);
|
||||
|
||||
play: async () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
|
||||
@@ -96,7 +96,7 @@ export const AgentPageHeader: FC<AgentPageHeaderProps> = ({
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
)}{" "}
|
||||
{children && <div className="flex items-center gap-2">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -94,10 +94,10 @@ export const ChatCostSummaryView: FC<ChatCostSummaryViewProps> = ({
|
||||
: 0;
|
||||
const usageProgressBarClass =
|
||||
usageProgressPercentage > 90
|
||||
? "bg-surface-red"
|
||||
? "bg-content-destructive"
|
||||
: usageProgressPercentage >= 75
|
||||
? "bg-surface-orange"
|
||||
: "bg-surface-green";
|
||||
? "bg-content-warning"
|
||||
: "bg-content-success";
|
||||
const usageLimitExceeded =
|
||||
showUsageLimitCard && usageLimitCurrentSpend >= usageLimitSpendMicros;
|
||||
const usageLimitStatusText = usageLimitExceeded
|
||||
|
||||
@@ -628,6 +628,15 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
visibleChatIDs.has(chatID),
|
||||
);
|
||||
|
||||
// Pre-compute the first non-empty time group so the filter
|
||||
// dropdown renders next to it without needing a mutable IIFE.
|
||||
const firstNonEmptyGroup = TIME_GROUPS.find((group) =>
|
||||
visibleRootIDs.some((id) => {
|
||||
const chat = chatById.get(id);
|
||||
return chat !== undefined && getTimeGroup(chat.updated_at) === group;
|
||||
}),
|
||||
);
|
||||
|
||||
// Auto-expand ancestors of the active chat so it's always visible.
|
||||
// Only runs when activeChatId changes — not on every parentById
|
||||
// recalculation — so user-initiated collapse is preserved.
|
||||
@@ -719,53 +728,6 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
<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>{" "}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
aria-label="Filter agents"
|
||||
className={cn(
|
||||
"h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary",
|
||||
archivedFilter === "archived" && "text-content-primary",
|
||||
)}
|
||||
>
|
||||
<FilterIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onSelect={() => onArchivedFilterChange?.("active")}
|
||||
>
|
||||
Active
|
||||
{archivedFilter === "active" && (
|
||||
<CheckIcon className="ml-auto h-3.5 w-3.5" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => onArchivedFilterChange?.("archived")}
|
||||
>
|
||||
Archived
|
||||
{archivedFilter === "archived" && (
|
||||
<CheckIcon className="ml-auto h-3.5 w-3.5" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{onCollapse && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -827,11 +789,22 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
<ChatTreeContext.Provider value={chatTreeCtx}>
|
||||
{visibleRootIDs.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border-default bg-surface-primary p-4 text-center text-xs text-content-secondary">
|
||||
{normalizedSearch
|
||||
? "No matching agents"
|
||||
: archivedFilter === "archived"
|
||||
? "No archived agents"
|
||||
: "No agents yet"}
|
||||
<p className="m-0">
|
||||
{normalizedSearch
|
||||
? "No matching agents"
|
||||
: archivedFilter === "archived"
|
||||
? "No archived agents"
|
||||
: "No agents yet"}
|
||||
</p>
|
||||
{archivedFilter === "archived" && (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-2 cursor-pointer border-none bg-transparent p-0 text-xs text-content-secondary hover:text-content-primary hover:underline"
|
||||
onClick={() => onArchivedFilterChange?.("active")}
|
||||
>
|
||||
← Back to active
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
@@ -851,8 +824,48 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
key={group}
|
||||
className="[&:not(:first-child)]:mt-3"
|
||||
>
|
||||
<div className="mb-1 ml-2.5 flex items-center justify-between text-xs font-medium text-content-secondary">
|
||||
<div className="mb-1 ml-2.5 -mr-0.5 flex items-center justify-between text-xs font-medium text-content-secondary">
|
||||
<span>{group}</span>
|
||||
{group === firstNonEmptyGroup && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
aria-label="Filter agents"
|
||||
className={cn(
|
||||
"h-7 w-7 min-w-0 text-content-secondary hover:text-content-primary",
|
||||
archivedFilter === "archived" &&
|
||||
"text-content-primary",
|
||||
)}
|
||||
>
|
||||
<FilterIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
onArchivedFilterChange?.("active")
|
||||
}
|
||||
>
|
||||
Active
|
||||
{archivedFilter === "active" && (
|
||||
<CheckIcon className="ml-auto h-3.5 w-3.5" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
onArchivedFilterChange?.("archived")
|
||||
}
|
||||
>
|
||||
Archived
|
||||
{archivedFilter === "archived" && (
|
||||
<CheckIcon className="ml-auto h-3.5 w-3.5" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{groupChats.map((chat) => (
|
||||
@@ -865,7 +878,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})}{" "}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@ export const UsageIndicator: FC = () => {
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/agents/analytics">View details</Link>
|
||||
<Link to="/agents/analytics">View usage</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
Reference in New Issue
Block a user