mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
fix(frontend): show org-wide settings badge beside title on org-defaults pages (#14031)
This commit is contained in:
@@ -192,12 +192,21 @@ describe("UserContextMenu", () => {
|
||||
|
||||
renderUserContextMenu({ type: "member", onClose: vi.fn, onOpenInviteModal: vi.fn });
|
||||
|
||||
// Wait for config to load and verify that navigation items are rendered (except organization-members/org which are filtered out)
|
||||
// In SaaS, personal LLM/Condenser/Verification routes are hidden in favor
|
||||
// of /settings/org-defaults/* (visible only when an org is selected, which
|
||||
// this test does not seed). Org-only and billing routes are also filtered.
|
||||
const personalLlmPaths = new Set([
|
||||
"/settings",
|
||||
"/settings/condenser",
|
||||
"/settings/verification",
|
||||
]);
|
||||
const expectedItems = SAAS_NAV_ITEMS.filter(
|
||||
(item) =>
|
||||
item.to !== "/settings/org-members" &&
|
||||
item.to !== "/settings/org" &&
|
||||
item.to !== "/settings/billing",
|
||||
item.to !== "/settings/billing" &&
|
||||
!item.to.startsWith("/settings/org-defaults") &&
|
||||
!personalLlmPaths.has(item.to),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -366,6 +375,15 @@ describe("UserContextMenu", () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
// The LLM nav item now lives under /settings/org-defaults, which only
|
||||
// appears when an org is selected. Seed a personal org for that.
|
||||
vi.spyOn(organizationService, "getOrganizations").mockResolvedValue({
|
||||
items: [MOCK_PERSONAL_ORG],
|
||||
currentOrgId: MOCK_PERSONAL_ORG.id,
|
||||
});
|
||||
useSelectedOrganizationStore.setState({
|
||||
organizationId: MOCK_PERSONAL_ORG.id,
|
||||
});
|
||||
|
||||
renderUserContextMenu({ type: "member", onClose: vi.fn, onOpenInviteModal: vi.fn });
|
||||
|
||||
|
||||
@@ -113,8 +113,12 @@ describe("useSettingsNavItems", () => {
|
||||
expect(findItemByPath(result.current, "/settings/billing")).toBeUndefined();
|
||||
expect(findItemByPath(result.current, "/settings/org")).toBeUndefined();
|
||||
expect(findItemByPath(result.current, "/settings/org-members")).toBeUndefined();
|
||||
// But should see other items
|
||||
expect(findItemByPath(result.current, "/settings")).toBeDefined();
|
||||
// Personal LLM/Condenser/Verification routes are hidden in SaaS;
|
||||
// members see the org-defaults equivalents (read-only on the page itself).
|
||||
expect(findItemByPath(result.current, "/settings")).toBeUndefined();
|
||||
expect(
|
||||
findItemByPath(result.current, "/settings/org-defaults"),
|
||||
).toBeDefined();
|
||||
expect(findItemByPath(result.current, "/settings/user")).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -375,9 +379,13 @@ describe("useSettingsNavItems", () => {
|
||||
expect(
|
||||
findItemByPath(result.current, "/settings/integrations"),
|
||||
).toBeUndefined();
|
||||
// Non-hidden pages should still be present
|
||||
// Personal LLM is hidden in SaaS; the org-defaults equivalent
|
||||
// shows up instead (an org is selected in this test's setup).
|
||||
expect(
|
||||
findItemByPath(result.current, "/settings"),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
findItemByPath(result.current, "/settings/org-defaults"),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
findItemByPath(result.current, "/settings/app"),
|
||||
@@ -406,8 +414,12 @@ describe("useSettingsNavItems", () => {
|
||||
expect(
|
||||
findItemByPath(result.current, "/settings/integrations"),
|
||||
).toBeDefined();
|
||||
// Personal LLM is hidden in SaaS; users see /settings/org-defaults instead.
|
||||
expect(
|
||||
findItemByPath(result.current, "/settings"),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
findItemByPath(result.current, "/settings/org-defaults"),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
findItemByPath(result.current, "/settings/app"),
|
||||
|
||||
@@ -554,7 +554,7 @@ describe("LlmSettingsScreen", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show info message for personal workspace", async () => {
|
||||
it("shows the personal info message for personal workspace in SaaS mode", async () => {
|
||||
vi.spyOn(SettingsService, "getSettings").mockResolvedValue(
|
||||
buildSettings(),
|
||||
);
|
||||
@@ -566,10 +566,9 @@ describe("LlmSettingsScreen", () => {
|
||||
organizations: [buildOrganization({ id: "1", is_personal: true })],
|
||||
});
|
||||
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
expect(
|
||||
screen.queryByTestId("llm-settings-info-message"),
|
||||
).not.toBeInTheDocument();
|
||||
await screen.findByTestId("llm-settings-info-message"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show info message in OSS mode", async () => {
|
||||
|
||||
@@ -122,6 +122,32 @@ describe("Settings Screen", () => {
|
||||
Component: () => <div data-testid="organization-settings-screen" />,
|
||||
path: "/settings/org",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="condenser-settings-screen" />,
|
||||
path: "/settings/condenser",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="verification-settings-screen" />,
|
||||
path: "/settings/verification",
|
||||
},
|
||||
{
|
||||
Component: () => (
|
||||
<div data-testid="org-default-llm-settings-screen" />
|
||||
),
|
||||
path: "/settings/org-defaults",
|
||||
},
|
||||
{
|
||||
Component: () => (
|
||||
<div data-testid="org-default-condenser-settings-screen" />
|
||||
),
|
||||
path: "/settings/org-defaults/condenser",
|
||||
},
|
||||
{
|
||||
Component: () => (
|
||||
<div data-testid="org-default-verification-settings-screen" />
|
||||
),
|
||||
path: "/settings/org-defaults/verification",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -792,6 +818,95 @@ describe("Settings Screen", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("OrgWideSettingsBadge display", () => {
|
||||
const seedSaasOrgContext = (
|
||||
org: typeof MOCK_TEAM_ORG_ACME | typeof MOCK_PERSONAL_ORG,
|
||||
user: Partial<OrganizationMember>,
|
||||
) => {
|
||||
mockQueryClient.clear();
|
||||
mockQueryClient.setQueryData(["web-client-config"], {
|
||||
app_mode: "saas",
|
||||
});
|
||||
mockQueryClient.setQueryData(["organizations"], {
|
||||
items: [org],
|
||||
currentOrgId: org.id,
|
||||
});
|
||||
useSelectedOrganizationStore.setState({ organizationId: org.id });
|
||||
vi.spyOn(organizationService, "getOrganizations").mockResolvedValue({
|
||||
items: [org],
|
||||
currentOrgId: org.id,
|
||||
});
|
||||
vi.spyOn(organizationService, "getMe").mockResolvedValue(
|
||||
createMockUser({ ...user, org_id: org.id }),
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockQueryClient.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
"/settings/org-defaults",
|
||||
"/settings/org-defaults/condenser",
|
||||
"/settings/org-defaults/verification",
|
||||
])(
|
||||
"renders the org-wide settings badge beside the title on %s for an admin in a team org in SaaS mode",
|
||||
async (path) => {
|
||||
seedSaasOrgContext(MOCK_TEAM_ORG_ACME, { role: "admin" });
|
||||
|
||||
renderSettingsScreen(path);
|
||||
|
||||
expect(
|
||||
await screen.findByTestId("org-wide-settings-badge"),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it("renders the badge on /settings/org-defaults for a non-admin member of a team org (read-only view)", async () => {
|
||||
seedSaasOrgContext(MOCK_TEAM_ORG_ACME, { role: "member" });
|
||||
|
||||
renderSettingsScreen("/settings/org-defaults");
|
||||
|
||||
await screen.findByTestId("org-default-llm-settings-screen");
|
||||
expect(
|
||||
await screen.findByTestId("org-wide-settings-badge"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render the badge on /settings/org-defaults when the selected organization is a personal org", async () => {
|
||||
seedSaasOrgContext(MOCK_PERSONAL_ORG, { role: "admin" });
|
||||
|
||||
renderSettingsScreen("/settings/org-defaults");
|
||||
|
||||
await screen.findByTestId("org-default-llm-settings-screen");
|
||||
expect(
|
||||
screen.queryByTestId("org-wide-settings-badge"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(["/settings/condenser", "/settings/verification"])(
|
||||
"does not render the badge on %s (personal-workspace-only route)",
|
||||
async (path) => {
|
||||
seedSaasOrgContext(MOCK_TEAM_ORG_ACME, { role: "admin" });
|
||||
|
||||
renderSettingsScreen(path);
|
||||
|
||||
await screen.findByTestId(
|
||||
path === "/settings/condenser"
|
||||
? "condenser-settings-screen"
|
||||
: "verification-settings-screen",
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId("org-wide-settings-badge"),
|
||||
).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFirstAvailablePath", () => {
|
||||
|
||||
@@ -41,7 +41,7 @@ vi.mock("#/query-client-config", () => ({
|
||||
}));
|
||||
|
||||
import { redirect } from "react-router";
|
||||
import { requirePersonalWorkspaceLoader } from "#/utils/org/personal-workspace-guard";
|
||||
import { requireOrgDefaultsRedirect as requirePersonalWorkspaceLoader } from "#/utils/org/saas-redirect-to-org-defaults-guard";
|
||||
|
||||
const createRequest = (pathname: string) => ({
|
||||
request: new Request(`http://localhost${pathname}`),
|
||||
@@ -65,14 +65,14 @@ describe("requirePersonalWorkspaceLoader", () => {
|
||||
expect(result).toEqual({ type: "redirect", path: "/settings/org-defaults" });
|
||||
});
|
||||
|
||||
it("allows access when the active org is the personal workspace", async () => {
|
||||
it("redirects to the org-defaults equivalent even when the active org is the personal workspace", async () => {
|
||||
storeOrgId = "personal-org";
|
||||
const guard = requirePersonalWorkspaceLoader("/settings/org-defaults");
|
||||
|
||||
const result = await guard(createRequest("/settings"));
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(redirect).not.toHaveBeenCalled();
|
||||
expect(redirect).toHaveBeenCalledWith("/settings/org-defaults");
|
||||
expect(result).toEqual({ type: "redirect", path: "/settings/org-defaults" });
|
||||
});
|
||||
|
||||
it("skips the guard entirely in OSS mode", async () => {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { OrgWideSettingsBadge } from "#/components/features/settings/org-wide-settings-badge";
|
||||
|
||||
export function OrgDefaultsBanner() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<OrgWideSettingsBadge />
|
||||
<p className="text-sm text-tertiary-alt">
|
||||
{t(I18nKey.SETTINGS$ORG_DEFAULTS_INFO)}
|
||||
</p>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function useSettingsNavItems(): SettingsNavRenderedItem[] {
|
||||
items = items.filter((item) => item.to !== "/settings/org-members");
|
||||
}
|
||||
|
||||
if (!hasPermission("edit_llm_settings") || !organizationId || isPersonalOrg) {
|
||||
if (!organizationId) {
|
||||
items = items.filter(
|
||||
(item) => !item.to.startsWith("/settings/org-defaults"),
|
||||
);
|
||||
@@ -82,7 +82,7 @@ export function useSettingsNavItems(): SettingsNavRenderedItem[] {
|
||||
"/settings/condenser",
|
||||
"/settings/verification",
|
||||
]);
|
||||
if (isTeamOrg) {
|
||||
if (isSaasMode) {
|
||||
items = items.filter((item) => !PERSONAL_LLM_PATHS.has(item.to));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SdkSectionPage } from "#/components/features/settings/sdk-settings/sdk-section-page";
|
||||
import { createPermissionGuard } from "#/utils/org/permission-guard";
|
||||
import { requirePersonalWorkspaceLoader } from "#/utils/org/personal-workspace-guard";
|
||||
import { requireOrgDefaultsRedirect } from "#/utils/org/saas-redirect-to-org-defaults-guard";
|
||||
|
||||
function CondenserSettingsScreen() {
|
||||
return (
|
||||
@@ -11,13 +11,13 @@ function CondenserSettingsScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const personalWorkspaceGuard = requirePersonalWorkspaceLoader(
|
||||
const orgDefaultsRedirectGuard = requireOrgDefaultsRedirect(
|
||||
"/settings/org-defaults/condenser",
|
||||
);
|
||||
const condenserPermissionGuard = createPermissionGuard("view_llm_settings");
|
||||
|
||||
export const clientLoader = async (args: { request: Request }) => {
|
||||
const blocked = await personalWorkspaceGuard(args);
|
||||
const blocked = await orgDefaultsRedirectGuard(args);
|
||||
if (blocked) return blocked;
|
||||
return condenserPermissionGuard(args);
|
||||
};
|
||||
|
||||
@@ -3,14 +3,13 @@ import { useSearchParams } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModelSelector } from "#/components/shared/modals/settings/model-selector";
|
||||
import { createPermissionGuard } from "#/utils/org/permission-guard";
|
||||
import { requirePersonalWorkspaceLoader } from "#/utils/org/personal-workspace-guard";
|
||||
import { requireOrgDefaultsRedirect } from "#/utils/org/saas-redirect-to-org-defaults-guard";
|
||||
import { useAgentSettingsSchema } from "#/hooks/query/use-agent-settings-schema";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||
import { HelpLink } from "#/ui/help-link";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
|
||||
import { useOrgTypeAndAccess } from "#/hooks/use-org-type-and-access";
|
||||
import {
|
||||
SdkSectionHeaderProps,
|
||||
SdkSectionPage,
|
||||
@@ -21,7 +20,6 @@ import {
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { Settings, SettingsSchema, SettingsScope } from "#/types/settings";
|
||||
import { OrgWideSettingsBadge } from "#/components/features/settings/org-wide-settings-badge";
|
||||
import { extractModelAndProvider } from "#/utils/extract-model-and-provider";
|
||||
import {
|
||||
inferInitialView,
|
||||
@@ -126,7 +124,6 @@ export function LlmSettingsScreen({
|
||||
settings?.agent_settings_schema,
|
||||
);
|
||||
const { data: config } = useConfig();
|
||||
const { isTeamOrg } = useOrgTypeAndAccess();
|
||||
|
||||
const [selectedProvider, setSelectedProvider] = React.useState<string | null>(
|
||||
null,
|
||||
@@ -166,11 +163,11 @@ export function LlmSettingsScreen({
|
||||
}, [searchParams, setSearchParams, t]);
|
||||
|
||||
const infoMessageKey = React.useMemo((): I18nKey | null => {
|
||||
if (!isSaasMode || !isTeamOrg) return null;
|
||||
if (!isSaasMode) return null;
|
||||
return scope === "org"
|
||||
? I18nKey.SETTINGS$ORG_DEFAULTS_INFO
|
||||
: I18nKey.SETTINGS$PERSONAL_AGENT_INFO;
|
||||
}, [isSaasMode, isTeamOrg, scope]);
|
||||
}, [isSaasMode, scope]);
|
||||
|
||||
const getInitialView = React.useCallback(
|
||||
(
|
||||
@@ -259,8 +256,6 @@ export function LlmSettingsScreen({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{scope === "org" ? <OrgWideSettingsBadge /> : null}
|
||||
|
||||
{infoMessageKey ? (
|
||||
<p
|
||||
data-testid="llm-settings-info-message"
|
||||
@@ -401,13 +396,13 @@ export function LlmSettingsScreen({
|
||||
);
|
||||
}
|
||||
|
||||
const personalWorkspaceGuard = requirePersonalWorkspaceLoader(
|
||||
const orgDefaultsRedirectGuard = requireOrgDefaultsRedirect(
|
||||
"/settings/org-defaults",
|
||||
);
|
||||
const llmPermissionGuard = createPermissionGuard("view_llm_settings");
|
||||
|
||||
export const clientLoader = async (args: { request: Request }) => {
|
||||
const blocked = await personalWorkspaceGuard(args);
|
||||
const blocked = await orgDefaultsRedirectGuard(args);
|
||||
if (blocked) return blocked;
|
||||
return llmPermissionGuard(args);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ function OrgDefaultCondenserSettingsScreen() {
|
||||
}
|
||||
|
||||
export const clientLoader = createPermissionGuard(
|
||||
"edit_llm_settings",
|
||||
"view_llm_settings",
|
||||
"/settings/condenser",
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createPermissionGuard } from "#/utils/org/permission-guard";
|
||||
import { LlmSettingsScreen } from "./llm-settings";
|
||||
|
||||
export const clientLoader = createPermissionGuard(
|
||||
"edit_llm_settings",
|
||||
"view_llm_settings",
|
||||
"/settings",
|
||||
);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ function OrgDefaultVerificationSettingsScreen() {
|
||||
}
|
||||
|
||||
export const clientLoader = createPermissionGuard(
|
||||
"edit_llm_settings",
|
||||
"view_llm_settings",
|
||||
"/settings/verification",
|
||||
);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
isSettingsPageHidden,
|
||||
getFirstAvailablePath,
|
||||
} from "#/utils/settings-utils";
|
||||
import { useMe } from "#/hooks/query/use-me";
|
||||
import { useOrgTypeAndAccess } from "#/hooks/use-org-type-and-access";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { OrgWideSettingsBadge } from "#/components/features/settings/org-wide-settings-badge";
|
||||
@@ -32,6 +31,12 @@ const SAAS_ONLY_PATHS = [
|
||||
"/settings/org",
|
||||
];
|
||||
|
||||
const ORG_WIDE_BADGE_PATHS = new Set<string>([
|
||||
"/settings/org-defaults",
|
||||
"/settings/org-defaults/condenser",
|
||||
"/settings/org-defaults/verification",
|
||||
]);
|
||||
|
||||
export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
|
||||
const url = new URL(request.url);
|
||||
const { pathname } = url;
|
||||
@@ -124,17 +129,14 @@ function SettingsScreen() {
|
||||
const location = useLocation();
|
||||
const matches = useMatches();
|
||||
const navItems = useSettingsNavItems();
|
||||
const { data: me } = useMe();
|
||||
const { data: config } = useConfig();
|
||||
const { isTeamOrg } = useOrgTypeAndAccess();
|
||||
|
||||
// Determine if we should show the org-wide settings badge
|
||||
// Only show for Admin/Owner roles on the LLM settings page in team orgs
|
||||
const isLlmSettingsPage = location.pathname === "/settings";
|
||||
const isAdminOrOwner = me?.role === "admin" || me?.role === "owner";
|
||||
// Only show for Admin/Owner roles on LLM/org-defaults pages in team orgs
|
||||
const isOrgWideBadgePath = ORG_WIDE_BADGE_PATHS.has(location.pathname);
|
||||
const isSaasMode = config?.app_mode === "saas";
|
||||
const shouldShowOrgWideBadge =
|
||||
isLlmSettingsPage && isAdminOrOwner && isTeamOrg && isSaasMode;
|
||||
const shouldShowOrgWideBadge = isOrgWideBadgePath && isTeamOrg && isSaasMode;
|
||||
|
||||
// Current section title for the main content area
|
||||
const currentSectionTitle = useMemo(() => {
|
||||
|
||||
@@ -6,13 +6,12 @@ import {
|
||||
SdkSectionHeaderProps,
|
||||
SdkSectionPage,
|
||||
} from "#/components/features/settings/sdk-settings/sdk-section-page";
|
||||
import { OrgWideSettingsBadge } from "#/components/features/settings/org-wide-settings-badge";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { SettingsScope } from "#/types/settings";
|
||||
import { createPermissionGuard } from "#/utils/org/permission-guard";
|
||||
import { requirePersonalWorkspaceLoader } from "#/utils/org/personal-workspace-guard";
|
||||
import { requireOrgDefaultsRedirect } from "#/utils/org/saas-redirect-to-org-defaults-guard";
|
||||
|
||||
const VERIFICATION_SCHEMA_EXCLUDE_KEYS = new Set([
|
||||
"confirmation_mode",
|
||||
@@ -20,7 +19,6 @@ const VERIFICATION_SCHEMA_EXCLUDE_KEYS = new Set([
|
||||
]);
|
||||
|
||||
function VerificationSettingsHeader({
|
||||
scope,
|
||||
confirmationMode,
|
||||
securityAnalyzer,
|
||||
isConversationSettingsDisabled,
|
||||
@@ -28,7 +26,6 @@ function VerificationSettingsHeader({
|
||||
onSecurityAnalyzerChange,
|
||||
renderTopContent,
|
||||
}: {
|
||||
scope: SettingsScope;
|
||||
confirmationMode: boolean;
|
||||
securityAnalyzer: string | null;
|
||||
isConversationSettingsDisabled: boolean;
|
||||
@@ -57,7 +54,6 @@ function VerificationSettingsHeader({
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{renderTopContent?.()}
|
||||
{scope === "org" ? <OrgWideSettingsBadge /> : null}
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
@@ -133,7 +129,6 @@ export function VerificationSettingsScreen({
|
||||
const buildHeader = React.useCallback(
|
||||
({ isDisabled }: SdkSectionHeaderProps) => (
|
||||
<VerificationSettingsHeader
|
||||
scope={scope}
|
||||
confirmationMode={confirmationMode}
|
||||
securityAnalyzer={securityAnalyzer}
|
||||
isConversationSettingsDisabled={isDisabled}
|
||||
@@ -148,7 +143,7 @@ export function VerificationSettingsScreen({
|
||||
renderTopContent={renderTopContent}
|
||||
/>
|
||||
),
|
||||
[confirmationMode, renderTopContent, scope, securityAnalyzer],
|
||||
[confirmationMode, renderTopContent, securityAnalyzer],
|
||||
);
|
||||
|
||||
const buildPayload = React.useCallback(
|
||||
@@ -194,13 +189,13 @@ export function VerificationSettingsScreen({
|
||||
);
|
||||
}
|
||||
|
||||
const personalWorkspaceGuard = requirePersonalWorkspaceLoader(
|
||||
const orgDefaultsRedirectGuard = requireOrgDefaultsRedirect(
|
||||
"/settings/org-defaults/verification",
|
||||
);
|
||||
const verificationPermissionGuard = createPermissionGuard("view_llm_settings");
|
||||
|
||||
export const clientLoader = async (args: { request: Request }) => {
|
||||
const blocked = await personalWorkspaceGuard(args);
|
||||
const blocked = await orgDefaultsRedirectGuard(args);
|
||||
if (blocked) return blocked;
|
||||
return verificationPermissionGuard(args);
|
||||
};
|
||||
|
||||
@@ -25,8 +25,8 @@ const fetchOrganizations = () =>
|
||||
// Fails open (returns null, allowing access) when org context cannot be
|
||||
// resolved — config fetch, org fetch, or active-org lookup. Backend permission
|
||||
// checks remain authoritative, so an outage degrades to "show the page" rather
|
||||
// than locking every user out of personal LLM settings.
|
||||
export const requirePersonalWorkspaceLoader =
|
||||
// than locking every user out of LLM settings.
|
||||
export const requireOrgDefaultsRedirect =
|
||||
(redirectPath: string = FALLBACK_REDIRECT_PATH) =>
|
||||
async ({ request }: { request: Request }) => {
|
||||
const config = await fetchConfig();
|
||||
@@ -51,8 +51,6 @@ export const requirePersonalWorkspaceLoader =
|
||||
const activeOrg = organizationsData?.items.find((o) => o.id === orgId);
|
||||
if (!activeOrg) return null;
|
||||
|
||||
if (activeOrg.is_personal === true) return null;
|
||||
|
||||
const currentPath = new URL(request.url).pathname;
|
||||
if (currentPath === redirectPath) return null;
|
||||
|
||||
Reference in New Issue
Block a user