From a3a0079bd2fcb0f29a0ea8e21ada1bc8599e42b5 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Tue, 18 Aug 2026 21:14:30 +0700 Subject: [PATCH] fix(site): stop redundant RBAC paywall error toast on Groups page (#28249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > 🤖 This PR was written by Coder Agents on behalf of Jake Howell. Fixes coder/coder#23898 / coder/coder#23898. ## Problem On a deployment without a Premium license, opening **Admin → Deployment settings → Groups** shows the Premium paywall *and* a redundant error toast in the bottom-right reading "Template RBAC is a Premium feature. Contact sales!". ## Root cause `GroupsPage.tsx` fired the paginated `groupsByOrganization` query unconditionally. Groups are gated behind the `template_rbac` (Premium) entitlement, so the request returned `403` ("Template RBAC is a Premium feature"), which a `useEffect` surfaced via `toast.error`. Meanwhile `GroupsPageView` already renders `PaywallPremium` when `groupsEnabled` is false, hence the duplicate messaging. The AI Governance page doesn't fire an entitlement-gated request, so it only shows the paywall. There's a second subtlety that made the bug load-path dependent: `selectFeatureVisibility` returns `{}` when unlicensed, so `template_rbac` is `undefined`, not `false`. React Query treats `enabled: undefined` as enabled, so a naive `enabled: groupsEnabled && ...` gate still fired the request on a fresh full page load (where entitlements were briefly in flight). Client-side navigation happened to have entitlements cached as `false`, so it looked fixed there but reproduced on hard reload. ## Fix Gate the groups query with `enabled: Boolean(groupsEnabled && organization)`. The `Boolean()` coercion is load-bearing: it turns the `undefined` entitlement into a real `false` so the request is genuinely skipped rather than defaulting to enabled. When the entitlement is missing there is no request, no error, and the paywall remains the single source of truth. Legitimate load failures (when the feature *is* entitled) still toast as before.
Investigation notes * `site/src/pages/GroupsPage/GroupsPage.tsx` — `groupsQuery` ran regardless of entitlement; the `groupsQuery.error` effect calls `toast.error`. * `site/src/pages/GroupsPage/GroupsPageView.tsx` — renders `PaywallPremium` when `!groupsEnabled`, independent of the query. * `site/src/modules/dashboard/entitlements.ts` — `getFeatureVisibility` returns `{}` when `!hasLicense`, so feature flags are `undefined` (not `false`) on unlicensed deployments. * `usePaginatedQuery` forwards `enabled` to the underlying `useQuery`, and its prefetch / invalid-page effects are no-ops while the query is disabled. * Backend source of the message: `enterprise/coderd/templates.go`.
## Testing Verified end-to-end on a local unlicensed `scripts/develop.sh` deployment (the exact repro condition): * Confirmed `GET /api/v2/organizations/coder/paginated-groups` returns `403 "Template RBAC is a Premium feature. Contact sales!"` — the toast's text. * **Before fix:** hard reload of `/deployment/groups` shows the error toast bottom-right alongside the paywall. * **After fix:** 3 consecutive hard reloads, no toast at any point (including the \~3s mark where it previously fired); paywall still renders correctly. * `pnpm --dir site lint:types` passes. Before/after screenshots are attached in the PR thread / chat. --- site/src/pages/GroupsPage/GroupsPage.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 5dd5846fb2..993f30fc1e 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -30,9 +30,10 @@ const GroupsPage: FC = () => { const { organization, showOrganizations } = useGroupsSettings(); const aibridgeVisible = Boolean(aibridge); const [searchParams, setSearchParams] = useSearchParams(); - const groupsQuery = usePaginatedQuery( - paginatedGroupsByOrganization(organization?.name ?? "", searchParams), - ); + const groupsQuery = usePaginatedQuery({ + ...paginatedGroupsByOrganization(organization?.name ?? "", searchParams), + enabled: Boolean(groupsEnabled && organization), + }); const filter = useFilter({ searchParams, onSearchParamsChange: setSearchParams,