Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0f00f7a

Browse files
fix(site): preserve organization in model setup links (#28631)
The model setup call to action opened AI settings without the current chat's organisation, so the models page would select a different one. We'll now carry the chat organisation through in the setup link. The AI settings sidebar had the same gap: the Models, Coder Agents and MCP servers links were hardcoded without `?org=`, so switching between them from a non-default organisation dropped you back to the default. Those three links now forward the current `org` search param; deployment-wide links (Providers, Templates, etc.) don't need it.
1 parent 6b3aec6 commit 0f00f7a

12 files changed

Lines changed: 281 additions & 65 deletions

site/src/components/Sidebar/Sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cn } from "cn";
22
import type { FC, ReactNode } from "react";
3-
import { NavLink } from "react-router";
3+
import { NavLink, type To } from "react-router";
44

55
interface SidebarProps {
66
children?: ReactNode;
@@ -15,7 +15,7 @@ export const Sidebar: FC<SidebarProps> = ({ className, children }) => {
1515

1616
interface SettingsSidebarNavItemProps {
1717
children?: ReactNode;
18-
href: string;
18+
href: To;
1919
end?: boolean;
2020
}
2121

site/src/modules/management/AISettingsSidebarView.stories.tsx

Lines changed: 113 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import { useLocation } from "react-router";
23
import { expect, userEvent, waitFor, within } from "storybook/test";
3-
import { reactRouterParameters } from "storybook-addon-remix-react-router";
4+
import {
5+
type RouterRoute,
6+
reactRouterParameters,
7+
} from "storybook-addon-remix-react-router";
48
import { organizationsPermissions } from "#/api/queries/organizations";
59
import {
610
MockDefaultOrganization,
@@ -16,27 +20,68 @@ import {
1620
import { AISettingsSidebar } from "./AISettingsSidebar";
1721
import AISettingsSidebarView from "./AISettingsSidebarView";
1822

23+
const LocationProbe = () => {
24+
const location = useLocation();
25+
return (
26+
<p role="status" aria-label="Current location" className="sr-only">
27+
{location.pathname}
28+
{location.search}
29+
</p>
30+
);
31+
};
32+
33+
const aiSettingsRoutes: [RouterRoute, ...RouterRoute[]] = [
34+
{ path: "/ai/settings/governance", useStoryElement: true },
35+
{ path: "/ai/settings/gateway-keys", useStoryElement: true },
36+
{ path: "/ai/settings/providers", useStoryElement: true },
37+
{ path: "/ai/settings/coder-agents", useStoryElement: true },
38+
{ path: "/ai/settings/models", useStoryElement: true },
39+
{
40+
path: "/ai/settings/organizations/:organization/models",
41+
useStoryElement: true,
42+
},
43+
{ path: "/ai/settings/mcp-servers", useStoryElement: true },
44+
{ path: "/ai/settings/mcp-servers/add", useStoryElement: true },
45+
{ path: "/ai/settings/templates", useStoryElement: true },
46+
{ path: "/ai/settings/instructions", useStoryElement: true },
47+
{ path: "/ai/settings/lifecycle", useStoryElement: true },
48+
];
49+
50+
const atLocation = (path: string, searchParams?: Record<string, string>) =>
51+
reactRouterParameters({
52+
location: { path, searchParams },
53+
routing: aiSettingsRoutes,
54+
});
55+
56+
const followLink = async (
57+
canvas: ReturnType<typeof within>,
58+
name: string,
59+
expectedLocation: string,
60+
) => {
61+
await userEvent.click(canvas.getByRole("link", { name }));
62+
await waitFor(() =>
63+
expect(
64+
canvas.getByRole("status", { name: "Current location" }).textContent,
65+
).toBe(expectedLocation),
66+
);
67+
};
68+
1969
const meta: Meta<typeof AISettingsSidebarView> = {
2070
title: "modules/management/AISettingsSidebarView",
2171
component: AISettingsSidebarView,
72+
decorators: [
73+
(Story) => (
74+
<>
75+
<Story />
76+
<LocationProbe />
77+
</>
78+
),
79+
],
2280
args: {
2381
permissions: MockPermissions,
2482
},
2583
parameters: {
26-
reactRouter: reactRouterParameters({
27-
location: { path: "/ai/settings/coder-agents" },
28-
routing: [
29-
{ path: "/ai/settings/governance", useStoryElement: true },
30-
{ path: "/ai/settings/gateway-keys", useStoryElement: true },
31-
{ path: "/ai/settings/providers", useStoryElement: true },
32-
{ path: "/ai/settings/coder-agents", useStoryElement: true },
33-
{ path: "/ai/settings/models", useStoryElement: true },
34-
{ path: "/ai/settings/mcp-servers", useStoryElement: true },
35-
{ path: "/ai/settings/templates", useStoryElement: true },
36-
{ path: "/ai/settings/instructions", useStoryElement: true },
37-
{ path: "/ai/settings/lifecycle", useStoryElement: true },
38-
],
39-
}),
84+
reactRouter: atLocation("/ai/settings/coder-agents"),
4085
},
4186
};
4287

@@ -55,10 +100,7 @@ export const CoderAgentsActive: Story = {
55100

56101
export const ModelsActive: Story = {
57102
parameters: {
58-
reactRouter: reactRouterParameters({
59-
location: { path: "/ai/settings/models" },
60-
routing: [{ path: "/ai/settings/models", useStoryElement: true }],
61-
}),
103+
reactRouter: atLocation("/ai/settings/models"),
62104
},
63105
play: async ({ canvasElement }) => {
64106
const canvas = within(canvasElement);
@@ -69,19 +111,36 @@ export const ModelsActive: Story = {
69111
},
70112
};
71113

114+
export const OrganizationParamPreserved: Story = {
115+
parameters: {
116+
reactRouter: atLocation("/ai/settings/models", { org: "my organization" }),
117+
},
118+
play: async ({ canvasElement }) => {
119+
const canvas = within(canvasElement);
120+
await followLink(
121+
canvas,
122+
"Coder Agents",
123+
"/ai/settings/coder-agents?org=my+organization",
124+
);
125+
await followLink(
126+
canvas,
127+
"MCP servers",
128+
"/ai/settings/mcp-servers?org=my+organization",
129+
);
130+
await followLink(
131+
canvas,
132+
"Models",
133+
"/ai/settings/models?org=my+organization",
134+
);
135+
await followLink(canvas, "Providers", "/ai/settings/providers");
136+
},
137+
};
138+
72139
export const ModelsActiveOnOrganizationRoute: Story = {
73140
parameters: {
74-
reactRouter: reactRouterParameters({
75-
location: {
76-
path: "/ai/settings/organizations/my-organization/models",
77-
},
78-
routing: [
79-
{
80-
path: "/ai/settings/organizations/:organization/models",
81-
useStoryElement: true,
82-
},
83-
],
84-
}),
141+
reactRouter: atLocation(
142+
"/ai/settings/organizations/my-organization/models",
143+
),
85144
},
86145
play: async ({ canvasElement }) => {
87146
const canvas = within(canvasElement);
@@ -94,19 +153,13 @@ export const ModelsActiveOnOrganizationRoute: Story = {
94153

95154
export const LifecycleActive: Story = {
96155
parameters: {
97-
reactRouter: reactRouterParameters({
98-
location: { path: "/ai/settings/lifecycle" },
99-
routing: [{ path: "/ai/settings/lifecycle", useStoryElement: true }],
100-
}),
156+
reactRouter: atLocation("/ai/settings/lifecycle"),
101157
},
102158
};
103159

104160
export const ProvidersActive: Story = {
105161
parameters: {
106-
reactRouter: reactRouterParameters({
107-
location: { path: "/ai/settings/providers" },
108-
routing: [{ path: "/ai/settings/providers", useStoryElement: true }],
109-
}),
162+
reactRouter: atLocation("/ai/settings/providers"),
110163
},
111164
play: async ({ canvasElement }) => {
112165
const canvas = within(canvasElement);
@@ -299,6 +352,28 @@ export const MCPServersForCreateOnlyAdmin: Story = {
299352
},
300353
};
301354

355+
export const MCPServersForCreateOnlyAdminPreservesOrganization: Story = {
356+
args: {
357+
permissions: {
358+
...MockNoPermissions,
359+
createAnyMCPServerConfig: true,
360+
},
361+
},
362+
parameters: {
363+
reactRouter: atLocation("/ai/settings/mcp-servers", {
364+
org: MockDefaultOrganization.name,
365+
}),
366+
},
367+
play: async ({ canvasElement }) => {
368+
const canvas = within(canvasElement);
369+
await followLink(
370+
canvas,
371+
"MCP servers",
372+
`/ai/settings/mcp-servers/add?org=${MockDefaultOrganization.name}`,
373+
);
374+
},
375+
};
376+
302377
export const MCPServersHiddenWithoutPermission: Story = {
303378
args: {
304379
permissions: {

site/src/modules/management/AISettingsSidebarView.tsx

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { cn } from "cn";
22
import type { FC, ReactNode } from "react";
3-
import { Link, NavLink, useMatch } from "react-router";
3+
import {
4+
Link,
5+
NavLink,
6+
type To,
7+
useMatch,
8+
useSearchParams,
9+
} from "react-router";
410
import {
511
Sidebar as BaseSidebar,
612
SettingsSidebarNavItem as SidebarNavItem,
@@ -9,6 +15,7 @@ import {
915
canAccessAnyChatModelConfig,
1016
type Permissions,
1117
} from "#/modules/permissions";
18+
import { modelOrganizationSearchParam } from "#/pages/AISettingsPage/ModelsPage/organizationModels";
1219

1320
interface AISettingsSidebarViewProps {
1421
/** Site-wide permissions. */
@@ -17,7 +24,7 @@ interface AISettingsSidebarViewProps {
1724
canShareOrganizationMCPServers?: boolean;
1825
}
1926

20-
const SubNavItem: FC<{ href: string; children?: ReactNode }> = ({
27+
const SubNavItem: FC<{ href: To; children?: ReactNode }> = ({
2128
href,
2229
children,
2330
}) => (
@@ -36,7 +43,19 @@ const SubNavItem: FC<{ href: string; children?: ReactNode }> = ({
3643
</NavLink>
3744
);
3845

39-
const ModelsSidebarNavItem: FC = () => {
46+
const organizationScopedPath = (
47+
pathname: string,
48+
organizationName: string | null,
49+
): To => ({
50+
pathname,
51+
search: organizationName
52+
? new URLSearchParams({
53+
[modelOrganizationSearchParam]: organizationName,
54+
}).toString()
55+
: "",
56+
});
57+
58+
const ModelsSidebarNavItem: FC<{ href: To }> = ({ href }) => {
4059
const legacyMatch = useMatch("/ai/settings/models/*");
4160
const organizationMatch = useMatch(
4261
"/ai/settings/organizations/:organization/models/*",
@@ -45,7 +64,7 @@ const ModelsSidebarNavItem: FC = () => {
4564

4665
return (
4766
<Link
48-
to="/ai/settings/models"
67+
to={href}
4968
aria-current={isActive ? "page" : undefined}
5069
className={cn(
5170
"relative text-sm text-content-secondary no-underline font-medium py-2 px-3 hover:bg-surface-secondary rounded-md transition ease-in-out duration-150",
@@ -62,6 +81,25 @@ const AISettingsSidebarView: FC<AISettingsSidebarViewProps> = ({
6281
canAccessOrganizationModels = false,
6382
canShareOrganizationMCPServers = false,
6483
}) => {
84+
const [searchParams] = useSearchParams();
85+
const organizationName = searchParams.get(modelOrganizationSearchParam);
86+
const modelsPath = organizationScopedPath(
87+
"/ai/settings/models",
88+
organizationName,
89+
);
90+
const coderAgentsPath = organizationScopedPath(
91+
"/ai/settings/coder-agents",
92+
organizationName,
93+
);
94+
const mcpServersPath = organizationScopedPath(
95+
"/ai/settings/mcp-servers",
96+
organizationName,
97+
);
98+
const addMCPServerPath = organizationScopedPath(
99+
"/ai/settings/mcp-servers/add",
100+
organizationName,
101+
);
102+
65103
return (
66104
<BaseSidebar>
67105
<div className="flex flex-col gap-1">
@@ -81,15 +119,15 @@ const AISettingsSidebarView: FC<AISettingsSidebarViewProps> = ({
81119
</SidebarNavItem>
82120
)}
83121
{(canAccessAnyChatModelConfig(permissions) ||
84-
canAccessOrganizationModels) && <ModelsSidebarNavItem />}
122+
canAccessOrganizationModels) && (
123+
<ModelsSidebarNavItem href={modelsPath} />
124+
)}
85125
{(permissions.editDeploymentConfig || canAccessOrganizationModels) && (
86-
<SidebarNavItem href="/ai/settings/coder-agents">
87-
Coder Agents
88-
</SidebarNavItem>
126+
<SidebarNavItem href={coderAgentsPath}>Coder Agents</SidebarNavItem>
89127
)}
90128
{permissions.editDeploymentConfig && (
91129
<div className="flex flex-col gap-1 ml-3 border-0 border-solid border-l border-l-border">
92-
<SubNavItem href="/ai/settings/mcp-servers">MCP servers</SubNavItem>
130+
<SubNavItem href={mcpServersPath}>MCP servers</SubNavItem>
93131
{permissions.updateAnyTemplate && (
94132
<SubNavItem href="/ai/settings/templates">Templates</SubNavItem>
95133
)}
@@ -112,8 +150,8 @@ const AISettingsSidebarView: FC<AISettingsSidebarViewProps> = ({
112150
permissions.updateAnyMCPServerConfig ||
113151
permissions.deleteAnyMCPServerConfig ||
114152
canShareOrganizationMCPServers
115-
? "/ai/settings/mcp-servers"
116-
: "/ai/settings/mcp-servers/add"
153+
? mcpServersPath
154+
: addMCPServerPath
117155
}
118156
>
119157
MCP servers

site/src/pages/AISettingsPage/ModelsPage/organizationModels.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "#/testHelpers/entities";
1111
import {
1212
organizationAddModelPath,
13+
organizationModelsPath,
1314
selectModelOrganization,
1415
useAccessibleModelOrganizations,
1516
} from "./organizationModels";
@@ -67,6 +68,12 @@ describe("selectModelOrganization", () => {
6768
});
6869
});
6970

71+
it("builds an organization-scoped models path", () => {
72+
expect(organizationModelsPath(MockOrganization2)).toBe(
73+
`/ai/settings/models?org=${MockOrganization2.name}`,
74+
);
75+
});
76+
7077
it("preserves auxiliary parameters in organization model paths", () => {
7178
const params = new URLSearchParams({
7279
provider: "openai",

site/src/pages/AISettingsPage/ModelsPage/organizationModels.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ const organizationModelSettingsPath = (
149149
return `/ai/settings/models${suffix}?${next.toString()}`;
150150
};
151151

152+
export const organizationModelsPath = (
153+
organization: Organization,
154+
searchParams?: URLSearchParams,
155+
): string => organizationModelSettingsPath(organization, "", searchParams);
156+
152157
export const organizationAddModelPath = (
153158
organization: Organization,
154159
searchParams?: URLSearchParams,
@@ -168,5 +173,5 @@ export const organizationModelPath = (
168173
export const useOrganizationModelsPath = (): string => {
169174
const { organization } = useOrganizationModels();
170175
const [searchParams] = useSearchParams();
171-
return organizationModelSettingsPath(organization, "", searchParams);
176+
return organizationModelsPath(organization, searchParams);
172177
};

0 commit comments

Comments
 (0)