From 7f7b13f0abfbbfd0ae66470e87c618f34d0b395c Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Tue, 31 Mar 2026 17:19:46 -0400 Subject: [PATCH] fix(site): share AI Bridge entitlement/permissions logic (#23834) Introduces a new `getAIBridgePermissions` method that all AI Bridge pages can use to restrict access/paywall. Also adds the paywall and alert to the session threads page bc I totes forgot. --------- Co-authored-by: Jake Howell --- .../AIBridgeSetupAlert.stories.tsx | 14 +++++++ .../pages/AIBridgePage/AIBridgeSetupAlert.tsx | 22 +++++++++++ .../ListSessionsPage/ListSessionsPage.tsx | 16 ++++---- .../ListSessionsPage/ListSessionsPageView.tsx | 20 +--------- .../RequestLogsPage/RequestLogsPage.tsx | 15 +++---- .../RequestLogsPage/RequestLogsPageView.tsx | 20 ++-------- .../SessionThreadsPage/SessionThreadsPage.tsx | 39 ++++++++++++++----- .../SessionThreadsPageView.tsx | 14 +++++++ .../AIBridgePage/getAIBridgePermissions.ts | 23 +++++++++++ 9 files changed, 121 insertions(+), 62 deletions(-) create mode 100644 site/src/pages/AIBridgePage/AIBridgeSetupAlert.stories.tsx create mode 100644 site/src/pages/AIBridgePage/AIBridgeSetupAlert.tsx create mode 100644 site/src/pages/AIBridgePage/getAIBridgePermissions.ts diff --git a/site/src/pages/AIBridgePage/AIBridgeSetupAlert.stories.tsx b/site/src/pages/AIBridgePage/AIBridgeSetupAlert.stories.tsx new file mode 100644 index 0000000000..0ce91dfeb7 --- /dev/null +++ b/site/src/pages/AIBridgePage/AIBridgeSetupAlert.stories.tsx @@ -0,0 +1,14 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { AIBridgeSetupAlert } from "./AIBridgeSetupAlert"; + +const meta: Meta = { + title: "pages/AIBridgePage/AIBridgeSetupAlert", + component: AIBridgeSetupAlert, +}; + +export default meta; +type Story = StoryObj; + +export const Alert: Story = { + args: {}, +}; diff --git a/site/src/pages/AIBridgePage/AIBridgeSetupAlert.tsx b/site/src/pages/AIBridgePage/AIBridgeSetupAlert.tsx new file mode 100644 index 0000000000..2f0ccbdfb2 --- /dev/null +++ b/site/src/pages/AIBridgePage/AIBridgeSetupAlert.tsx @@ -0,0 +1,22 @@ +import type { FC } from "react"; +import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert"; +import { Link } from "#/components/Link/Link"; +import { docs } from "#/utils/docs"; + +export const AIBridgeSetupAlert: FC = () => { + return ( + + + AI Bridge is included in your license, but not set up yet. + + + You have access to AI Governance, but it still needs to be setup. Check + out the{" "} + + AI Bridge + {" "} + documentation to get started. + + + ); +}; diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx index b6ffb1d601..f34cfb3a30 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx @@ -8,6 +8,7 @@ import { usePaginatedQuery } from "#/hooks/usePaginatedQuery"; import { useDashboard } from "#/modules/dashboard/useDashboard"; import { RequirePermission } from "#/modules/permissions/RequirePermission"; import { pageTitle } from "#/utils/page"; +import { getAIBridgePermissions } from "../getAIBridgePermissions"; import { useClientFilterMenu } from "../RequestLogsPage/RequestLogsFilter/ClientFilter"; import { useProviderFilterMenu } from "../RequestLogsPage/RequestLogsFilter/ProviderFilter"; import { ListSessionsPageView } from "./ListSessionsPageView"; @@ -17,15 +18,11 @@ const AISessionListPage: FC = () => { const { entitlements } = useDashboard(); const navigate = useNavigate(); - // Users are allowed to view their own request logs via the API, - // but this page is only visible if the feature is enabled and the user - // has the `viewAnyAIBridgeInterception` permission. - // (as its defined in the Admin settings dropdown). - const isEntitled = - entitlements.features.aibridge.entitlement === "entitled" || - entitlements.features.aibridge.entitlement === "grace_period"; - const isEnabled = entitlements.features.aibridge.enabled; - const hasPermission = permissions.viewAnyAIBridgeInterception; + const { isEntitled, isEnabled, hasPermission } = getAIBridgePermissions( + entitlements, + permissions, + ); + const canViewSessions = isEntitled && hasPermission; const [searchParams, setSearchParams] = useSearchParams(); @@ -33,6 +30,7 @@ const AISessionListPage: FC = () => { ...paginatedSessions(searchParams), enabled: canViewSessions, }); + const filter = useFilter({ searchParams, onSearchParamsChange: setSearchParams, diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx index c4f07cf5a4..34805a7585 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.tsx @@ -1,8 +1,6 @@ import { InfoIcon } from "lucide-react"; import type { ComponentProps, FC, PropsWithChildren } from "react"; import type { AIBridgeSession } from "#/api/typesGenerated"; -import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert"; -import { Link } from "#/components/Link/Link"; import { PaginationContainer, type PaginationResult, @@ -23,8 +21,8 @@ import { TooltipProvider, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { docs } from "#/utils/docs"; import { DATE_FORMAT, formatDateTime } from "#/utils/time"; +import { AIBridgeSetupAlert } from "../AIBridgeSetupAlert"; import { ListSessionsFilter } from "./ListSessionsFilter"; import { ListSessionsRow } from "./ListSessionsRow"; @@ -70,21 +68,7 @@ export const ListSessionsPageView: FC = ({ } if (!isAISessionsEnabled) { - return ( - - - AI Bridge is included in your license, but not set up yet. - - - You have access to AI Governance, but it still needs to be setup. - Check out the{" "} - - AI Bridge - {" "} - documentation to get started. - - - ); + return ; } const utcOffset = formatDateTime(new Date(), DATE_FORMAT.UTC_OFFSET); diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPage.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPage.tsx index 895de8fb6b..c9cbc5a57f 100644 --- a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPage.tsx +++ b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPage.tsx @@ -8,6 +8,7 @@ import { usePaginatedQuery } from "#/hooks/usePaginatedQuery"; import { useDashboard } from "#/modules/dashboard/useDashboard"; import { RequirePermission } from "#/modules/permissions/RequirePermission"; import { pageTitle } from "#/utils/page"; +import { getAIBridgePermissions } from "../getAIBridgePermissions"; import { useClientFilterMenu } from "./RequestLogsFilter/ClientFilter"; import { useModelFilterMenu } from "./RequestLogsFilter/ModelFilter"; import { useProviderFilterMenu } from "./RequestLogsFilter/ProviderFilter"; @@ -17,15 +18,11 @@ const RequestLogsPage: FC = () => { const { permissions } = useAuthenticated(); const { entitlements } = useDashboard(); - // Users are allowed to view their own request logs via the API, - // but this page is only visible if the feature is enabled and the user - // has the `viewAnyAIBridgeInterception` permission. - // (as its defined in the Admin settings dropdown). - const isEntitled = - entitlements.features.aibridge.entitlement === "entitled" || - entitlements.features.aibridge.entitlement === "grace_period"; - const isEnabled = entitlements.features.aibridge.enabled; - const hasPermission = permissions.viewAnyAIBridgeInterception; + const { isEntitled, isEnabled, hasPermission } = getAIBridgePermissions( + entitlements, + permissions, + ); + const canViewRequestLogs = isEntitled && hasPermission; const [searchParams, setSearchParams] = useSearchParams(); diff --git a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPageView.tsx b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPageView.tsx index 8ac7cce7e1..69c8635743 100644 --- a/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPageView.tsx +++ b/site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsPageView.tsx @@ -1,6 +1,6 @@ import type { ComponentProps, FC } from "react"; import type { AIBridgeInterception } from "#/api/typesGenerated"; -import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert"; +import { Alert } from "#/components/Alert/Alert"; import { Link } from "#/components/Link/Link"; import { PaginationContainer, @@ -16,7 +16,7 @@ import { } from "#/components/Table/Table"; import { TableEmpty } from "#/components/TableEmpty/TableEmpty"; import { TableLoader } from "#/components/TableLoader/TableLoader"; -import { docs } from "#/utils/docs"; +import { AIBridgeSetupAlert } from "../AIBridgeSetupAlert"; import { RequestLogsFilter } from "./RequestLogsFilter/RequestLogsFilter"; import { RequestLogsRow } from "./RequestLogsRow/RequestLogsRow"; @@ -42,21 +42,7 @@ export const RequestLogsPageView: FC = ({ } if (!isRequestLogsEnabled) { - return ( - - - AI Bridge is included in your license, but not set up yet. - - - You have access to AI Governance, but it still needs to be setup. - Check out the{" "} - - AI Bridge - {" "} - documentation to get started. - - - ); + return ; } return ( diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPage.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPage.tsx index e162127421..b98201bc04 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPage.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPage.tsx @@ -2,14 +2,29 @@ import type { FC } from "react"; import { useInfiniteQuery } from "react-query"; import { useParams } from "react-router"; import { infiniteSessionThreads } from "#/api/queries/aiBridge"; +import { useAuthenticated } from "#/hooks/useAuthenticated"; +import { useDashboard } from "#/modules/dashboard/useDashboard"; +import { RequirePermission } from "#/modules/permissions/RequirePermission"; +import { pageTitle } from "#/utils/page"; +import { getAIBridgePermissions } from "../getAIBridgePermissions"; import { SessionThreadsPageView } from "./SessionThreadsPageView"; const SessionThreadsPage: FC = () => { + const { permissions } = useAuthenticated(); + const { entitlements } = useDashboard(); + + const { isEntitled, isEnabled, hasPermission } = getAIBridgePermissions( + entitlements, + permissions, + ); + + const canViewSessionThreads = isEntitled && hasPermission; + const { sessionId } = useParams() as { sessionId: string }; const sessionQuery = useInfiniteQuery({ ...infiniteSessionThreads(sessionId), - enabled: !!sessionId, + enabled: canViewSessionThreads, }); const firstPage = sessionQuery.data?.pages[0]; @@ -17,14 +32,20 @@ const SessionThreadsPage: FC = () => { sessionQuery.data?.pages.flatMap((page) => page.threads) ?? []; return ( - + + {pageTitle("Session Threads", "AI Bridge")} + + + ); }; diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx index 00f523a919..5de34810b0 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx @@ -7,12 +7,14 @@ import type { } from "#/api/typesGenerated"; import { Button } from "#/components/Button/Button"; import { Loader } from "#/components/Loader/Loader"; +import { PaywallAIGovernance } from "#/components/Paywall/PaywallAIGovernance"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; +import { AIBridgeSetupAlert } from "../AIBridgeSetupAlert"; import { SessionSummaryTable } from "./SessionSummaryTable"; import { SessionTimeline } from "./SessionTimeline/SessionTimeline"; @@ -43,6 +45,8 @@ interface SessionThreadsPageViewProps { hasNextPage: boolean; isFetchingNextPage: boolean; onFetchNextPage: () => void; + isAISessionsEnabled: boolean; + isAISessionsEntitled: boolean; } export const SessionThreadsPageView: FC = ({ @@ -52,7 +56,17 @@ export const SessionThreadsPageView: FC = ({ hasNextPage, isFetchingNextPage, onFetchNextPage, + isAISessionsEnabled, + isAISessionsEntitled, }) => { + if (!isAISessionsEntitled) { + return ; + } + + if (!isAISessionsEnabled) { + return ; + } + // calculate the total number of tool calls across all loaded threads const toolCallCount = threads.reduce( (acc, thread) => acc + (thread.agentic_actions?.length ?? 0), diff --git a/site/src/pages/AIBridgePage/getAIBridgePermissions.ts b/site/src/pages/AIBridgePage/getAIBridgePermissions.ts new file mode 100644 index 0000000000..71e45f63b4 --- /dev/null +++ b/site/src/pages/AIBridgePage/getAIBridgePermissions.ts @@ -0,0 +1,23 @@ +import type { Entitlements } from "#/api/typesGenerated"; +import type { Permissions } from "#/modules/permissions"; + +// Users are allowed to view their own request logs via the API, +// but an AI Bridge page is only visible if the feature is enabled and +// the user has the `viewAnyAIBridgeInterception` permission. (as it's +// defined in the Admin settings dropdown). +export const getAIBridgePermissions = ( + entitlements: Entitlements, + permissions: Permissions, +) => { + // the user is entitled if they have either "entitled" or "grace_period" + // status for the AI Bridge feature + const isEntitled = + entitlements.features.aibridge.entitlement === "entitled" || + entitlements.features.aibridge.entitlement === "grace_period"; + // the feature is enabled if it's toggled on in the admin settings + const isEnabled = entitlements.features.aibridge.enabled; + // the user has permission if they have the `viewAnyAIBridgeInterception` permission + const hasPermission = permissions.viewAnyAIBridgeInterception; + + return { isEntitled, isEnabled, hasPermission }; +};