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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions site/src/modules/management/AISettingsSidebarView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ export const OrganizationOnlyRoleCanAccessModels: Story = {
const canvas = within(canvasElement);
await expect(canvas.getByRole("link", { name: "Models" })).toBeVisible();
await expect(
canvas.queryByRole("link", { name: "Coder Agents" }),
).not.toBeInTheDocument();
canvas.getByRole("link", { name: "Coder Agents" }),
).toBeVisible();
},
};

Expand Down
32 changes: 15 additions & 17 deletions site/src/modules/management/AISettingsSidebarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,24 +80,22 @@ const AISettingsSidebarView: FC<AISettingsSidebarViewProps> = ({
)}
{(canAccessAnyChatModelConfig(permissions) ||
canAccessOrganizationModels) && <ModelsSidebarNavItem />}
{(permissions.editDeploymentConfig || canAccessOrganizationModels) && (
<SidebarNavItem href="/ai/settings/coder-agents">
Coder Agents
</SidebarNavItem>
)}
{permissions.editDeploymentConfig && (
<>
<SidebarNavItem href="/ai/settings/coder-agents">
Coder Agents
</SidebarNavItem>
<div className="flex flex-col gap-1 ml-3 border-0 border-solid border-l border-l-border">
<SubNavItem href="/ai/settings/mcp-servers">
MCP servers
</SubNavItem>
{permissions.updateTemplates && (
<SubNavItem href="/ai/settings/templates">Templates</SubNavItem>
)}
<SubNavItem href="/ai/settings/instructions">
Instructions
</SubNavItem>
<SubNavItem href="/ai/settings/lifecycle">Lifecycle</SubNavItem>
</div>
</>
<div className="flex flex-col gap-1 ml-3 border-0 border-solid border-l border-l-border">
<SubNavItem href="/ai/settings/mcp-servers">MCP servers</SubNavItem>
{permissions.updateTemplates && (
<SubNavItem href="/ai/settings/templates">Templates</SubNavItem>
)}
<SubNavItem href="/ai/settings/instructions">
Instructions
</SubNavItem>
<SubNavItem href="/ai/settings/lifecycle">Lifecycle</SubNavItem>
</div>
)}
{!permissions.editDeploymentConfig &&
(permissions.viewAnyMCPServerConfigs ||
Expand Down
71 changes: 69 additions & 2 deletions site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FC } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useSearchParams } from "react-router";
import {
chatAdvisorConfig,
chatComputerUseProvider,
Expand All @@ -8,17 +9,42 @@ import {
updateChatComputerUseProvider,
updateChatPersonalModelOverridesAdminSettings,
} from "#/api/queries/chats";
import { organizationsPermissions } from "#/api/queries/organizations";
import { Loader } from "#/components/Loader/Loader";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
import {
modelOrganizationSearchParam,
selectModelOrganization,
useAccessibleModelOrganizations,
} from "#/pages/AISettingsPage/ModelsPage/organizationModels";
import { pageTitle } from "#/utils/page";
import { CoderAgentsPageView } from "./CoderAgentsPageView";
import { OrganizationAgentSettings } from "./OrganizationAgentSettings";

const CoderAgentsPage: FC = () => {
const { permissions } = useAuthenticated();
const { experiments } = useDashboard();
const { experiments, organizations } = useDashboard();
const queryClient = useQueryClient();
const [searchParams, setSearchParams] = useSearchParams();
const canEditDeploymentConfig = permissions.editDeploymentConfig;
const accessibleOrganizationsQuery =
useAccessibleModelOrganizations(organizations);
const organizationSelection = selectModelOrganization(
accessibleOrganizationsQuery.organizations,
searchParams.get(modelOrganizationSearchParam),
);
Comment thread
ethanndickson marked this conversation as resolved.
const activeOrganization = organizationSelection.organization;
const organizationPermissionsQuery = useQuery({
...organizationsPermissions(
activeOrganization ? [activeOrganization.id] : undefined,
),
enabled: activeOrganization !== undefined,
});
const activeOrganizationPermissions = activeOrganization
? organizationPermissionsQuery.data?.[activeOrganization.id]
: undefined;
const showAdvisorSettings = experiments.includes("chat-advisor");
const showVirtualDesktopSettings = experiments.includes(
"chat-virtual-desktop",
Expand All @@ -44,11 +70,52 @@ const CoderAgentsPage: FC = () => {
const saveComputerUseProviderMutation = useMutation(
updateChatComputerUseProvider(queryClient),
);
const canAccessOrganizationSettings =
accessibleOrganizationsQuery.organizations.length > 0;
const isFeatureVisible =
canEditDeploymentConfig ||
canAccessOrganizationSettings ||
accessibleOrganizationsQuery.isLoading ||
accessibleOrganizationsQuery.error !== null;
const organizationSettings =
activeOrganization &&
organizationPermissionsQuery.data === undefined &&
organizationPermissionsQuery.error == null ? (
<Loader />
) : activeOrganization &&
organizationPermissionsQuery.data !== undefined ? (
<OrganizationAgentSettings
organization={activeOrganization}
canEdit={
!organizationSelection.requestedOrganizationDenied &&
(activeOrganizationPermissions?.editChatModelConfigs ?? false)
}
showAdvisor={showAdvisorSettings}
/>
) : undefined;

return (
<RequirePermission isFeatureVisible={canEditDeploymentConfig}>
<RequirePermission isFeatureVisible={isFeatureVisible}>
<title>{pageTitle("Coder Agents", "AI Settings")}</title>
<CoderAgentsPageView
organization={activeOrganization}
organizations={accessibleOrganizationsQuery.organizations}
onSelectOrganization={(organization) => {
const next = new URLSearchParams(searchParams);
next.set(modelOrganizationSearchParam, organization.name);
setSearchParams(next);
}}
organizationAccessError={
accessibleOrganizationsQuery.partialError ??
accessibleOrganizationsQuery.error
}
organizationPermissionsError={organizationPermissionsQuery.error}
requestedOrganizationDenied={
organizationSelection.requestedOrganizationDenied
}
isOrganizationAccessLoading={accessibleOrganizationsQuery.isLoading}
organizationSettings={organizationSettings}
canEditDeploymentConfig={canEditDeploymentConfig}
adminOverridesData={personalOverridesQuery.data}
adminOverridesError={personalOverridesQuery.error}
onRetryAdminOverrides={() => void personalOverridesQuery.refetch()}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import { expect, fn, screen, userEvent, waitFor, within } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import {
MockDefaultOrganization,
MockOrganization2,
} from "#/testHelpers/entities";
import {
CoderAgentsPageView,
type CoderAgentsPageViewProps,
} from "./CoderAgentsPageView";

const defaultArgs: CoderAgentsPageViewProps = {
organization: MockDefaultOrganization,
organizations: [MockDefaultOrganization, MockOrganization2],
onSelectOrganization: fn(),
requestedOrganizationDenied: false,
isOrganizationAccessLoading: false,
organizationSettings: <div>Organization override controls</div>,
canEditDeploymentConfig: true,
adminOverridesData: { allow_users: true },
onSaveAdminOverrides: fn(),
isSavingAdminOverrides: false,
Expand Down Expand Up @@ -51,7 +62,10 @@ export const Default: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await expect(
canvas.getAllByRole("link", { name: "Defaults & overrides" })[0],
canvas.getByRole("heading", { name: "Organization settings" }),
).toBeVisible();
await expect(
canvas.getByRole("heading", { name: "Deployment settings" }),
).toBeVisible();
await expect(canvas.getByText("Advisor")).toBeVisible();
const maxUses = canvas.getByRole("spinbutton", { name: "Uses / turn" });
Expand All @@ -76,3 +90,129 @@ export const WithoutAdvisor: Story = {
await expect(canvas.queryByText("Advisor")).not.toBeInTheDocument();
},
};

export const SelectOrganization: Story = {
args: { onSelectOrganization: fn() },
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await userEvent.click(
canvas.getByRole("button", {
name: new RegExp(MockDefaultOrganization.display_name, "i"),
}),
);
await userEvent.click(
await screen.findByRole("option", {
name: new RegExp(MockOrganization2.display_name, "i"),
}),
);
await expect(args.onSelectOrganization).toHaveBeenCalledWith(
MockOrganization2,
);
},
};

export const OrganizationOnly: Story = {
args: { canEditDeploymentConfig: false },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByRole("heading", { name: "Organization settings" }),
).toBeVisible();
await expect(
canvas.queryByRole("heading", { name: "Deployment settings" }),
).not.toBeInTheDocument();
},
};

export const OrganizationDiscoveryLoading: Story = {
args: { isOrganizationAccessLoading: true },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByRole("status", { name: "Loading organization settings" }),
).toBeVisible();
await expect(
canvas.queryByRole("heading", { name: "Organization settings" }),
).not.toBeInTheDocument();
await expect(
canvas.queryByText("Organization override controls"),
).not.toBeInTheDocument();
await expect(
canvas.getByRole("heading", { name: "Deployment settings" }),
).toBeVisible();
},
};

export const DeploymentOnly: Story = {
args: {
organization: undefined,
organizations: [],
organizationSettings: undefined,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.queryByRole("heading", { name: "Organization settings" }),
).not.toBeInTheDocument();
await expect(
canvas.getByRole("heading", { name: "Deployment settings" }),
).toBeVisible();
},
};

export const SingleOrganization: Story = {
args: { organizations: [MockDefaultOrganization] },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.queryByRole("button", {
name: new RegExp(MockDefaultOrganization.display_name, "i"),
}),
).not.toBeInTheDocument();
},
};

export const InaccessibleRequestedOrganization: Story = {
args: { requestedOrganizationDenied: true },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByRole("alert")).toHaveTextContent(
"requested organization is not available",
);
await expect(
canvas.getByText("Organization override controls"),
).toBeVisible();
},
};

export const IndependentErrors: Story = {
args: {
organizationAccessError: new Error("Failed to load another organization"),
adminOverridesError: new Error("Failed to load deployment setting"),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByText("Failed to load another organization"),
).toBeVisible();
await expect(
canvas.getByText("Failed to load deployment setting"),
).toBeVisible();
await expect(
canvas.getByText("Organization override controls"),
).toBeVisible();
},
};

export const Mobile: Story = {
parameters: { viewport: { defaultViewport: "mobile1" } },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByRole("heading", { name: "Organization settings" }),
).toBeVisible();
await expect(
canvas.getByRole("heading", { name: "Deployment settings" }),
).toBeVisible();
},
};
Loading
Loading