diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormFields.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormFields.tsx
index 2d8aff1304a..5301b6672ec 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormFields.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormFields.tsx
@@ -1,7 +1,12 @@
import type { FormikContextType } from "formik";
import { ChevronDownIcon, ChevronRightIcon, InfoIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
-import { Link as RouterLink } from "react-router";
+import {
+ Link as RouterLink,
+ useLocation,
+ useNavigate,
+ useSearchParams,
+} from "react-router";
import { getVisibleProviderFields } from "#/api/chatModelOptions";
import type * as TypesGen from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
@@ -19,6 +24,7 @@ import {
} from "#/components/InputGroup/InputGroup";
import { Label } from "#/components/Label/Label";
import { Link } from "#/components/Link/Link";
+import { OrganizationField } from "#/components/OrganizationAutocomplete/OrganizationAutocomplete";
import { Spinner } from "#/components/Spinner/Spinner";
import {
Tooltip,
@@ -40,7 +46,12 @@ import type {
import { cn } from "#/utils/cn";
import { docs } from "#/utils/docs";
import type { FormHelpers } from "#/utils/formUtils";
-import { useOrganizationModelsPath } from "../organizationModels";
+import {
+ creatableModelOrganizations,
+ selectModelOrganizationPath,
+ useOrganizationModels,
+ useOrganizationModelsPath,
+} from "../organizationModels";
import { ModelFormProviderSelect } from "./ModelFormProviderSelect";
const CollapsibleSection: FC<{
@@ -140,6 +151,41 @@ export const ModelFormFields: FC<{
setShowAdvanced,
}) => {
const modelsPath = useOrganizationModelsPath();
+ const { organization, accessibleOrganizations, permissionsByOrganization } =
+ useOrganizationModels();
+ const location = useLocation();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const selectableOrganizations = isEditing
+ ? accessibleOrganizations
+ : creatableModelOrganizations(
+ accessibleOrganizations,
+ permissionsByOrganization,
+ );
+ const organizationField = isEditing ? (
+
+ ) : selectableOrganizations.length > 1 ? (
+ {
+ void navigate(
+ selectModelOrganizationPath(
+ location.pathname,
+ nextOrganization,
+ searchParams,
+ ),
+ );
+ }}
+ />
+ ) : null;
const hasProviderConfigFields =
getVisibleProviderFields(selectedProviderState.provider).length > 0;
@@ -249,6 +295,7 @@ export const ModelFormFields: FC<{
+ {organizationField}
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx
index 13572da3df6..c424aa40f44 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx
@@ -1,17 +1,35 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
+import {
+ MockDefaultOrganization,
+ MockOrganizationPermissions,
+} from "#/testHelpers/entities";
import { withToaster } from "#/testHelpers/storybook";
+import { OrganizationModelsContext } from "../organizationModels";
import {
MockAnthropicProviderState,
MockOpenAIProviderState,
} from "../testFixtures";
import { ModelForm } from "./ModelForm";
+const withOrganizationModels = (Story: React.FC) => (
+
+
+
+);
+
const meta: Meta = {
title: "pages/AISettingsPage/ModelsPage/ModelForm",
component: ModelForm,
- decorators: [withToaster],
+ decorators: [withToaster, withOrganizationModels],
args: {
providerStates: [MockOpenAIProviderState, MockAnthropicProviderState],
selectedProviderState: MockOpenAIProviderState,
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
index 01a39ce6508..9aa149a8dd8 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { expect, userEvent, within } from "storybook/test";
+import { expect, fn, userEvent, within } from "storybook/test";
import { Table, TableBody } from "#/components/Table/Table";
import { mockClaude, mockGPT5 } from "../testFixtures";
import { ModelRow } from "./ModelRow";
@@ -32,49 +32,48 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-// Control case for the effective-status logic: when both `hasProvider` and
-// `providerEnabled` are true, the status badge must reflect the persisted
-// enabled flag as-is. Any regression that inverts this collapses every model
-// to "Disabled" in the list.
+// Control case: a healthy, enabled model renders no status badge at all.
export const WithProvider: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("OpenAI")).toBeInTheDocument();
- await expect(canvas.getByText("Enabled")).toBeInTheDocument();
+ expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
+ expect(canvas.queryByText("Disabled")).not.toBeInTheDocument();
+ expect(canvas.queryByText("Unavailable")).not.toBeInTheDocument();
await expect(canvas.queryByText("Unset")).not.toBeInTheDocument();
},
};
-// When the provider is missing (soft-deleted or otherwise unavailable) the
-// Provider column shows "Unset" and the status collapses to "Disabled" even
-// though the persisted model.enabled flag is true. An info icon next to the
-// label reveals a tooltip explaining that the connected provider has been
-// deleted.
+// A missing (soft-deleted) provider shows "Unset" plus the "Unavailable"
+// notice even though the persisted model.enabled flag is true.
export const WithoutProviderForcesDisabled: Story = {
args: {
model: { ...mockClaude, enabled: true },
providerLabel: "",
hasProvider: false,
providerEnabled: false,
+ onClick: fn(),
},
- play: async ({ canvasElement }) => {
+ play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Unset")).toBeInTheDocument();
- await expect(canvas.getByText("Disabled")).toBeInTheDocument();
- await expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
+ expect(canvas.queryByText("Disabled")).not.toBeInTheDocument();
- const info = canvas.getByLabelText("Provider status");
- await userEvent.hover(info);
+ // The badge is keyboard-focusable and must open its tooltip without
+ // activating the clickable row.
+ const notice = canvas.getByRole("button", { name: "Unavailable" });
+ notice.focus();
const tooltip = await within(document.body).findByRole("tooltip");
await expect(tooltip).toHaveTextContent(
"The provider connected to this model has been deleted.",
);
+ await userEvent.keyboard("{Enter}");
+ expect(args.onClick).not.toHaveBeenCalled();
},
};
-// When the provider exists but is disabled, the label still renders (the
-// provider is set) but the status collapses to "Disabled" because the model
-// is not usable.
+// A disabled provider keeps its label but the model shows the "Unavailable"
+// notice because it is not usable.
export const DisabledProviderForcesDisabled: Story = {
args: {
model: { ...mockClaude, enabled: true, is_default: false },
@@ -85,15 +84,21 @@ export const DisabledProviderForcesDisabled: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Anthropic")).toBeInTheDocument();
- await expect(canvas.getByText("Disabled")).toBeInTheDocument();
- await expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
+ const notice = canvas.getByRole("button", { name: "Unavailable" });
+ await expect(notice).toBeInTheDocument();
+ expect(canvas.queryByText("Disabled")).not.toBeInTheDocument();
await expect(canvas.queryByText("Unset")).not.toBeInTheDocument();
+
+ await userEvent.hover(notice);
+ const tooltip = await within(document.body).findByRole("tooltip");
+ await expect(tooltip).toHaveTextContent(
+ "The provider connected to this model is disabled.",
+ );
},
};
-// A disabled model with an enabled provider keeps its provider label but
-// stays "Disabled". This exercises the enabled=false path so the "Unset"
-// wording is only tied to the missing provider case.
+// enabled=false with a healthy provider: "Disabled" badge beside the name,
+// no "Unavailable" notice, and no "Unset" wording.
export const DisabledModelWithProvider: Story = {
args: {
model: { ...mockClaude, enabled: false, is_default: false },
@@ -104,7 +109,9 @@ export const DisabledModelWithProvider: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Anthropic")).toBeInTheDocument();
- await expect(canvas.getByText("Disabled")).toBeInTheDocument();
+ const nameCell = canvas.getByRole("cell", { name: /Claude Sonnet 4.5/ });
+ await expect(within(nameCell).getByText("Disabled")).toBeInTheDocument();
+ expect(canvas.queryByText("Unavailable")).not.toBeInTheDocument();
await expect(canvas.queryByText("Unset")).not.toBeInTheDocument();
},
};
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
index ee1e7e81e14..25b6e1432db 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
@@ -1,4 +1,4 @@
-import { ChevronRightIcon, InfoIcon } from "lucide-react";
+import { ChevronRightIcon } from "lucide-react";
import type { FC } from "react";
import type { ChatModel } from "#/api/typesGenerated";
import { Avatar } from "#/components/Avatar/Avatar";
@@ -11,6 +11,7 @@ import {
} from "#/components/Tooltip/Tooltip";
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
import { ProviderIcon } from "#/pages/AISettingsPage/ProvidersPage/components/ProviderIcon";
+import { cn } from "#/utils/cn";
type ModelRowProps = {
model: ChatModel;
@@ -39,8 +40,17 @@ export const ModelRow: FC = ({
const clickableProps = useClickableTableRow({ onClick });
const displayName = model.display_name || model.model;
// Models whose provider is missing or disabled cannot be used, so the
- // status column reflects that regardless of the persisted enabled flag.
- const isEffectivelyEnabled = model.enabled && hasProvider && providerEnabled;
+ // status cell surfaces that regardless of the persisted enabled flag.
+ const providerNotice = !hasProvider
+ ? "The provider connected to this model has been deleted."
+ : !providerEnabled
+ ? "The provider connected to this model is disabled."
+ : null;
+
+ // Keep tooltip activation from triggering the clickable row's navigation.
+ const stopPropagation = (event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ };
return (
@@ -48,7 +58,10 @@ export const ModelRow: FC = ({
-
- }
- >
+
AI Gateway
- Monitor and manage AI requests across your deployment.
+ Monitor and manage AI requests across your deployment.{" "}
+
diff --git a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx
index bc81d061c79..dbdb2aba3ae 100644
--- a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx
+++ b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx
@@ -58,7 +58,7 @@ export const NotEntitled: Story = {
const cta = canvas.getByRole("link", { name: "Start trial for free" });
await expect(cta).toHaveAttribute("href", "/deployment/premium");
await expect(
- canvas.getByRole("link", { name: /Read the docs/ }),
+ canvas.getByRole("link", { name: /View docs/ }),
).toHaveAttribute("href", docs("/admin/setup/appearance"));
await expect(
canvas.queryByRole("form", { name: "Appearance settings" }),
diff --git a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
index 7c5484aa318..f17a113a983 100644
--- a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
+++ b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
@@ -49,14 +49,11 @@ export const AppearanceSettingsPageView: FC<
return (
-
- }
- >
+ Appearance
- Customize the look and feel of your Coder deployment.
+ Customize the look and feel of your Coder deployment.{" "}
+
diff --git a/site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.stories.tsx
index 0b5ac04cd8c..8880f583ea3 100644
--- a/site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.stories.tsx
+++ b/site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.stories.tsx
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, within } from "storybook/test";
+import { docs } from "#/utils/docs";
import { ExternalAuthSettingsPageView } from "./ExternalAuthSettingsPageView";
const meta: Meta = {
@@ -49,6 +50,9 @@ export const Page: Story = {
await expect(
canvas.getByRole("link", { name: "Start trial for free" }),
).toHaveAttribute("href", "/deployment/premium");
+ await expect(
+ canvas.getByRole("link", { name: /View docs/ }),
+ ).toHaveAttribute("href", docs("/admin/external-auth"));
},
};
diff --git a/site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx
index c27648515e2..8c5cb354762 100644
--- a/site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx
+++ b/site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx
@@ -33,13 +33,12 @@ export const ExternalAuthSettingsPageView: FC<
> = ({ config, isEntitled, canViewPremium }) => {
return (
<>
- }
- >
+ External Authentication
Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and
- OpenID Connect to authenticate developers with external services.
+ OpenID Connect to authenticate developers with external services.{" "}
+
diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx
index 56f5ef031ce..48163500d23 100644
--- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx
+++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx
@@ -70,19 +70,15 @@ const IdpOrgSyncPage: FC = () => {
-
-
-
- }
+ actions={}
>
Organization IdP Sync
Automatically assign users to an organization based on their IdP
- claims.
+ claims.{" "}
+
{!isIdpSyncEnabled ? (
diff --git a/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPageView.stories.tsx
index 6aa7dc2c5c9..9c8f1c9465d 100644
--- a/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPageView.stories.tsx
+++ b/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPageView.stories.tsx
@@ -1,5 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
+import { expect, within } from "storybook/test";
import type { SerpentGroup } from "#/api/typesGenerated";
+import { docs } from "#/utils/docs";
import { NetworkSettingsPageView } from "./NetworkSettingsPageView";
const group: SerpentGroup = {
@@ -67,4 +69,18 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-export const Page: Story = {};
+export const Page: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const docsLinks = canvas.getAllByRole("link", { name: /View docs/ });
+ await expect(docsLinks).toHaveLength(2);
+ await expect(docsLinks[0]).toHaveAttribute(
+ "href",
+ docs("/admin/networking"),
+ );
+ await expect(docsLinks[1]).toHaveAttribute(
+ "href",
+ docs("/admin/networking/port-forwarding"),
+ );
+ },
+};
diff --git a/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPageView.tsx
index aa218f6f88d..b545af5497a 100644
--- a/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPageView.tsx
+++ b/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPageView.tsx
@@ -27,12 +27,14 @@ export const NetworkSettingsPageView: FC = ({
}) => (
- }
- >
+ Network
- Configure your deployment connectivity.
+ Configure your deployment connectivity.{" "}
+
@@ -44,19 +46,17 @@ export const NetworkSettingsPageView: FC = ({
-
- }
- >
+
Port Forwarding
Port forwarding lets developers securely access processes on their
- Coder workspace from a local machine.
+ Coder workspace from a local machine.{" "}
+
diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx
index 131e29f1223..58bf9fdf4e2 100644
--- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx
+++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx
@@ -1,9 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { spyOn, userEvent, within } from "storybook/test";
+import { expect, spyOn, userEvent, within } from "storybook/test";
import { API } from "#/api/api";
import { selectTemplatesByGroup } from "#/api/queries/notifications";
import type { DeploymentValues } from "#/api/typesGenerated";
import { MockSystemNotificationTemplates } from "#/testHelpers/entities";
+import { docs } from "#/utils/docs";
import { NotificationEvents } from "./NotificationEvents";
import { baseMeta } from "./storybookUtils";
@@ -36,6 +37,15 @@ export const SMTPNotConfigured: Story = {
},
} as DeploymentValues,
},
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await expect(
+ canvas.getByRole("link", { name: /View docs/ }),
+ ).toHaveAttribute(
+ "href",
+ docs("/admin/monitoring/notifications#smtp-email"),
+ );
+ },
};
export const WebhookNotConfigured: Story = {
@@ -53,6 +63,12 @@ export const WebhookNotConfigured: Story = {
},
} as DeploymentValues,
},
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await expect(
+ canvas.getByRole("link", { name: /View docs/ }),
+ ).toHaveAttribute("href", docs("/admin/monitoring/notifications#webhook"));
+ },
};
export const ChangeMethod: Story = {
diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx
index a0000ea5722..e12199edae6 100644
--- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx
+++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx
@@ -8,7 +8,7 @@ import {
} from "#/api/queries/notifications";
import type { DeploymentValues } from "#/api/typesGenerated";
import { Alert } from "#/components/Alert/Alert";
-import { Button } from "#/components/Button/Button";
+import { Link } from "#/components/Link/Link";
import {
Select,
SelectContent,
@@ -66,15 +66,14 @@ export const NotificationEvents: FC = ({
severity="warning"
prominent
actions={
-
-
- Read the docs
-
-
+
+ View docs
+ (opens in new tab)
+
}
>
Webhook notifications are enabled, but not properly configured.
@@ -86,15 +85,14 @@ export const NotificationEvents: FC = ({
severity="warning"
prominent
actions={
-
-
- Read the docs
-
-
+
+ View docs
+ (opens in new tab)
+
}
>
SMTP notifications are enabled but not properly configured.
diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.stories.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.stories.tsx
index 4cc9b242df2..504edd2375c 100644
--- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.stories.tsx
+++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.stories.tsx
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { userEvent, within } from "storybook/test";
+import { expect, userEvent, within } from "storybook/test";
import {
customNotificationTemplatesKey,
notificationDispatchMethodsKey,
@@ -10,6 +10,7 @@ import {
MockNotificationMethodsResponse,
MockSystemNotificationTemplates,
} from "#/testHelpers/entities";
+import { docs } from "#/utils/docs";
import NotificationsPage from "./NotificationsPage";
import { baseMeta } from "./storybookUtils";
@@ -64,6 +65,20 @@ export const LoadingDispatchMethods: Story = {
export const Events: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
+ const docsLinks = canvas.getAllByRole("link", { name: /View docs/ });
+ await expect(docsLinks).toHaveLength(3);
+ await expect(docsLinks[0]).toHaveAttribute(
+ "href",
+ docs("/admin/monitoring/notifications"),
+ );
+ await expect(docsLinks[1]).toHaveAttribute(
+ "href",
+ docs("/admin/monitoring/notifications#webhook"),
+ );
+ await expect(docsLinks[2]).toHaveAttribute(
+ "href",
+ docs("/admin/monitoring/notifications#smtp-email"),
+ );
// System notification templates
await canvas.findByText("Template Events");
diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx
index 2214d8ac106..55c492e557c 100644
--- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx
+++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx
@@ -78,16 +78,14 @@ const NotificationsPage: FC = () => {
<>
Codestin Search App
-
+ Notifications
+
+ Control delivery methods for notifications on this deployment.{" "}
- }
- >
- Notifications
-
- Control delivery methods for notifications on this deployment.
diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.stories.tsx
index 031b3fcc070..0c7a2c6666a 100644
--- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.stories.tsx
+++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.stories.tsx
@@ -167,7 +167,7 @@ export const AddApplicationIsScopedToApplicationsTab: Story = {
// The docs link is tab-agnostic and stays put, which is what makes the
// other one's disappearance a scoping decision rather than a quirk.
- const docsLink = canvas.getByRole("link", { name: /read the docs/i });
+ const docsLink = canvas.getByRole("link", { name: /view docs/i });
await expect(docsLink).toBeVisible();
await userEvent.click(canvas.getByRole("tab", { name: "Settings" }));
@@ -175,7 +175,7 @@ export const AddApplicationIsScopedToApplicationsTab: Story = {
canvas.queryByRole("link", { name: "Add application" }),
).not.toBeInTheDocument();
await expect(
- canvas.getByRole("link", { name: /read the docs/i }),
+ canvas.getByRole("link", { name: /view docs/i }),
).toBeVisible();
await userEvent.click(canvas.getByRole("tab", { name: "Applications" }));
diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx
index 13556be8130..91d948f1ea1 100644
--- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx
+++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx
@@ -141,23 +141,10 @@ const OAuth2AppsSettingsPageView: FC = ({
return (
- {/*
- * The header sits outside the tabs, so a tab-specific action here would
- * promise to act on content it navigates away from. The docs link is
- * tab-agnostic, so it stays on both.
- */}
- {canCreateApp && activeTab === "applications" && (
-
- )}
-
- >
+ canCreateApp &&
+ activeTab === "applications" &&
}
>
OAuth2 applications
@@ -168,7 +155,12 @@ const OAuth2AppsSettingsPageView: FC = ({
*/}
Register applications to use Coder as an OAuth2 provider
- {settings && ", and configure how this deployment behaves as one"}.
+ {settings && ", and configure how this deployment behaves as one"}.{" "}
+
diff --git a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.stories.tsx
index 87ef677ef32..bc152e37bc6 100644
--- a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.stories.tsx
+++ b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.stories.tsx
@@ -82,9 +82,16 @@ export const OSS: Story = {
await expect(
canvas.getByRole("link", { name: "Start trial for free" }),
).toHaveAttribute("href", "/deployment/premium");
- await expect(
- canvas.getByRole("link", { name: /Read the docs/ }),
- ).toHaveAttribute("href", docs("/admin/security/audit-logs"));
+ const docsLinks = canvas.getAllByRole("link", { name: /View docs/ });
+ await expect(docsLinks).toHaveLength(2);
+ await expect(docsLinks[0]).toHaveAttribute(
+ "href",
+ docs("/admin/security/audit-logs"),
+ );
+ await expect(docsLinks[1]).toHaveAttribute(
+ "href",
+ docs("/admin/monitoring"),
+ );
await expect(
canvas.queryByText("Audit Logs Retention"),
).not.toBeInTheDocument();
diff --git a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.tsx
index 62b53185445..6c7f8a9ba09 100644
--- a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.tsx
+++ b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPageView.tsx
@@ -23,11 +23,7 @@ export const ObservabilitySettingsPageView: FC<
return (
-
- }
- >
+ Observability
@@ -36,7 +32,11 @@ export const ObservabilitySettingsPageView: FC<
Audit Logging
- Allow auditors to monitor user operations in your deployment.
+ Allow auditors to monitor user operations in your deployment.{" "}
+
@@ -66,7 +66,11 @@ export const ObservabilitySettingsPageView: FC<
Monitoring
- Monitoring your Coder application with logs and metrics.
+ Monitoring your Coder application with logs and metrics.{" "}
+
diff --git a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx
index f6f84b222c1..d07aa4255ea 100644
--- a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx
+++ b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx
@@ -1,5 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
+import { expect, within } from "storybook/test";
import { MockDeploymentDAUResponse } from "#/testHelpers/entities";
+import { docs } from "#/utils/docs";
import { OverviewPageView } from "./OverviewPageView";
const meta: Meta = {
@@ -44,7 +46,14 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-export const Page: Story = {};
+export const Page: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await expect(
+ canvas.getByRole("link", { name: /View docs/ }),
+ ).toHaveAttribute("href", docs("/admin/setup"));
+ },
+};
export const allExperimentsEnabled: Story = {
args: {
diff --git a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.tsx b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.tsx
index 4a2c75fa4b4..87dfc033e5c 100644
--- a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.tsx
+++ b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.tsx
@@ -32,12 +32,11 @@ export const OverviewPageView: FC = ({
}) => {
return (
<>
- }
- >
+ General
- Information about your Coder deployment.
+ Information about your Coder deployment.{" "}
+
diff --git a/site/src/pages/DeploymentSettingsPage/PremiumPage/PremiumPage.tsx b/site/src/pages/DeploymentSettingsPage/PremiumPage/PremiumPage.tsx
index cfef257a06e..98a239b7df3 100644
--- a/site/src/pages/DeploymentSettingsPage/PremiumPage/PremiumPage.tsx
+++ b/site/src/pages/DeploymentSettingsPage/PremiumPage/PremiumPage.tsx
@@ -49,17 +49,14 @@ const PremiumPage: FC = () => {
<>
Codestin Search App
-
- Review Coder system requirements
-
- }
- >
+ Start a Coder trial
For enterprises ready to achieve world-class security, scalability,
- and developer experience.
+ and developer experience.{" "}
+
+ Review Coder system requirements
+
= ({
Security
- Ensure your Coder deployment is secure.
+ Ensure your Coder deployment is secure.{" "}
+
@@ -57,14 +61,12 @@ export const SecuritySettingsPageView: FC = ({
-
- }
- >
-
+
+
Browser-Only Connections{" "}
{featureBrowserOnlyEnabled ? : }
@@ -72,13 +74,14 @@ export const SecuritySettingsPageView: FC = ({
Block all workspace access via SSH, port forward, and other
- non-browser connections.
+ non-browser connections.{" "}
+
-
- {featureBrowserOnlyEnabled ? : }
-
{!isBrowserOnlyEntitled ? (
= {
export default meta;
type Story = StoryObj;
-export const Page: Story = {};
+export const Page: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const docsLinks = canvas.getAllByRole("link", { name: /View docs/ });
+ await expect(docsLinks).toHaveLength(2);
+ await expect(docsLinks[0]).toHaveAttribute(
+ "href",
+ docs("/admin/users/oidc-auth"),
+ );
+ await expect(docsLinks[1]).toHaveAttribute(
+ "href",
+ docs("/admin/users/github-auth"),
+ );
+ },
+};
diff --git a/site/src/pages/DeploymentSettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.tsx
index 1a5087d95cb..c460292d9c6 100644
--- a/site/src/pages/DeploymentSettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.tsx
+++ b/site/src/pages/DeploymentSettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.tsx
@@ -39,16 +39,16 @@ export const UserAuthSettingsPageView = ({
User Authentication
-
- }
- >
+
Login with OpenID Connect
- Set up authentication to login with OpenID Connect.
+ Set up authentication to login with OpenID Connect.{" "}
+
@@ -64,16 +64,16 @@ export const UserAuthSettingsPageView = ({
-
- }
- >
+
Login with GitHub
- Set up authentication to login with GitHub.
+ Set up authentication to login with GitHub.{" "}
+
diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx
index fcf2eff29e4..cf0ff7fbef9 100644
--- a/site/src/pages/GroupsPage/GroupsPage.tsx
+++ b/site/src/pages/GroupsPage/GroupsPage.tsx
@@ -113,15 +113,12 @@ const GroupsPage: FC = () => {
Organize your deployment into multiple platform teams with unique
- provisioners, templates, groups, and members.
+ provisioners, templates, groups, and members.{" "}
+
diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx
index 611117319c4..059fe7cd8a4 100644
--- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx
+++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx
@@ -82,14 +82,11 @@ const CustomRolesPage: FC = () => {
-
- }
- >
+ Roles
- Manage roles for this organization.
+ Manage roles for this organization.{" "}
+
diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx
index 3fb537523c1..46511527dda 100644
--- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx
+++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx
@@ -123,15 +123,12 @@ const IdpSyncPage: FC = () => {
{title}
-
- }
- >
+ IdP Sync
Automatically assign groups or roles to a user based on their IdP
- claims.
+ claims.{" "}
+
{!isIdpSyncEnabled ? (
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx
index f3e88e1262a..01e2d8dd858 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx
@@ -54,7 +54,7 @@ export const JobRow: FC = ({ job, defaultIsOpen = false }) => {
- {job.type}
+ {job.type}
{job.metadata.template_name !== "" ? (
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx
index a7854060d3d..2c573f2f45c 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx
@@ -3,6 +3,7 @@ import { useState } from "react";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import type { ProvisionerJob } from "#/api/typesGenerated";
import { MockOrganization, MockProvisionerJob } from "#/testHelpers/entities";
+import { docs } from "#/utils/docs";
import { daysAgo } from "#/utils/time";
import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView";
@@ -29,7 +30,17 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-export const Default: Story = {};
+export const Default: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await expect(
+ canvas.getByRole("link", { name: /View docs/ }),
+ ).toHaveAttribute(
+ "href",
+ docs("/admin/provisioners/manage-provisioner-jobs"),
+ );
+ },
+};
export const OrganizationNotFound: Story = {
args: {
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx
index 8d09ad1b6e3..42bd3437ea4 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx
@@ -110,7 +110,9 @@ const OrganizationProvisionerJobsPageView: FC<
Provisioner Jobs are the individual tasks assigned to Provisioners
when the workspaces are being built.{" "}
- View docs
+
+ View docs
+
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx
index ce34141fe94..1b905046cf8 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx
@@ -101,7 +101,7 @@ export const Paywalled: Story = {
const cta = canvas.getByRole("link", { name: "Start trial for free" });
await expect(cta).toHaveAttribute("href", "/deployment/premium");
await expect(
- canvas.getByRole("link", { name: /Read the docs/ }),
+ canvas.getByRole("link", { name: /View docs/ }),
).toHaveAttribute("href", docs("/admin/provisioners"));
},
};
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx
index da9be24980a..4800dc1a101 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx
@@ -51,12 +51,11 @@ export const OrganizationProvisionerKeysPageView: FC<
return (
- }
- >
+ Provisioner Keys
- Manage provisioner keys used to authenticate provisioner instances.
+ Manage provisioner keys used to authenticate provisioner instances.{" "}
+
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx
index 340a75608bd..36f38e98d2a 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx
@@ -77,7 +77,7 @@ export const Paywall: Story = {
const cta = canvas.getByRole("link", { name: "Start trial for free" });
await expect(cta).toHaveAttribute("href", "/deployment/premium");
await expect(
- canvas.getByRole("link", { name: /Read the docs/ }),
+ canvas.getByRole("link", { name: /View docs/ }),
).toHaveAttribute("href", docs("/admin/provisioners"));
},
};
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx
index 916765ef8a8..8389ca33a7c 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx
@@ -61,13 +61,12 @@ export const OrganizationProvisionersPageView: FC<
}) => {
return (
- }
- >
+ Provisioners
Coder server runs provisioner daemons which execute terraform during
- workspace and template builds.
+ workspace and template builds.{" "}
+
diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx
index aaef36b2896..99ad3485e07 100644
--- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx
@@ -41,16 +41,13 @@ const TemplatePermissionsPage: FC = () => {
Codestin Search App
-
+ Permissions
+
+ Manage which members and groups can use this template.{" "}
- }
- >
- Permissions
-
- Manage which members and groups can use this template.
diff --git a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.stories.tsx b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.stories.tsx
index 6c440a44365..d6c5c5688e0 100644
--- a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.stories.tsx
+++ b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.stories.tsx
@@ -109,7 +109,7 @@ export const Loaded: Story = {
await expect(canvas.getByText("env var + file")).toBeInTheDocument();
await expect(canvas.getByText("not injected")).toBeInTheDocument();
- const docsLink = canvas.getByRole("link", { name: "Read the docs" });
+ const docsLink = canvas.getByRole("link", { name: /View docs/ });
await expect(docsLink).toHaveAttribute(
"href",
expect.stringContaining("/user-guides/user-secrets"),
diff --git a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.tsx b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.tsx
index cfac6530ab9..5d356592e8e 100644
--- a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.tsx
+++ b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.tsx
@@ -1,4 +1,4 @@
-import { PlusIcon, SquareArrowOutUpRightIcon } from "lucide-react";
+import { PlusIcon } from "lucide-react";
import { type FC, useRef, useState } from "react";
import type {
CreateUserSecretRequest,
@@ -11,6 +11,7 @@ import { Button } from "#/components/Button/Button";
import {
SettingsHeader,
SettingsHeaderDescription,
+ SettingsHeaderDocsLink,
SettingsHeaderTitle,
} from "#/components/SettingsHeader/SettingsHeader";
import { docs } from "#/utils/docs";
@@ -86,29 +87,18 @@ export const SecretsPageView: FC = ({