fix(web): Reporting — Global Admin banner and tenant selector in the page header, section control row back to its old layout

The reporting layout (ReportingSidebar) now owns the override-only scope, renders the banner above the header and the selector next to the title like Fleet Health, and hands the scope to the sections through ReportingScopeContext (one hook instance). Delegated callers get neither. Fixes the five-line wrap of the section control row on phones; internal gitlink.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-09-10 03:27:19 +02:00
parent d2221eb0b9
commit 6804bcfd4e
4 changed files with 45 additions and 13 deletions
@@ -0,0 +1,17 @@
"use client";
import { createContext, useContext } from "react";
import type { GlobalAdminScope } from "@/hooks";
// One scope instance for the reporting subtree: the header (ReportingSidebar) owns the banner and the
// tenant selector, the sections read the selection. A second useGlobalAdminScope() call would hold its
// own selection state and never see the header's changes.
const ReportingScopeContext = createContext<GlobalAdminScope | null>(null);
export const ReportingScopeProvider = ReportingScopeContext.Provider;
export function useReportingScope(): GlobalAdminScope {
const scope = useContext(ReportingScopeContext);
if (!scope) throw new Error("useReportingScope must be used inside the reporting layout");
return scope;
}
@@ -1,17 +1,34 @@
"use client";
import { useGlobalAdminScope } from "@/hooks";
import { GlobalAdminBanner, globalAdminSubtitle } from "@/components/GlobalAdminBanner";
import { TenantScopeSelector } from "@/components/TenantScopeSelector";
import { ReportingScopeProvider } from "./ReportingScopeContext";
export function ReportingSidebar({ children }: { children: React.ReactNode }) {
// Override-only scope (always a concrete tenant, defaulting to the caller's own). A delegated (MSP)
// caller gets neither banner nor selector here: the organization route never lists a managed
// tenant's accounts, so the reporting sections stay on their own tenant for them.
const scope = useGlobalAdminScope();
const crossTenant = scope.isGlobalAdmin && !scope.isDelegatedScope;
return (
<>
<ReportingScopeProvider value={scope}>
<GlobalAdminBanner show={crossTenant} subtitle={globalAdminSubtitle(scope)} />
<header className="bg-white shadow">
<div className="py-6 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-normal text-gray-900">Reporting</h1>
<p className="text-sm text-gray-500 mt-1">AI agent access and usage statistics</p>
<div className="flex flex-wrap items-center justify-between gap-y-3">
<div>
<h1 className="text-2xl font-normal text-gray-900">Reporting</h1>
<p className="text-sm text-gray-500 mt-1">AI agent access and usage statistics</p>
</div>
{crossTenant && <TenantScopeSelector scope={scope} />}
</div>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{children}
</div>
</>
</ReportingScopeProvider>
);
}
@@ -6,8 +6,7 @@ import { useAuth } from "../../../../contexts/AuthContext";
import { ApiError, apiErrorText, fetchJson } from "@/lib/apiClient";
import { api } from "@/lib/api";
import { scopedApi } from "@/lib/scopedApi";
import { useGlobalAdminScope } from "@/hooks";
import { TenantScopeSelector } from "@/components/TenantScopeSelector";
import { useReportingScope } from "../ReportingScopeContext";
import { DocsLink } from "@/components/DocsLink";
import { DOCS_PATHS } from "@/lib/docsPaths";
import type {
@@ -88,11 +87,11 @@ export function SectionMcpUsage() {
const { getAccessToken, user } = useAuth();
const canSeeOrganization = !!(user?.isTenantAdmin || user?.isGlobalAdmin);
// Global-admin tenant scope (override-only: always a concrete tenant, defaulting to the caller's own).
// Only the organization cards follow the selection — the caller's own quota and request history never
// do. A delegated (MSP) caller keeps the member path: the organization route never lists a managed
// tenant's accounts, so the selector stays hidden for them.
const scope = useGlobalAdminScope();
// Global-admin tenant scope from the reporting header (override-only: always a concrete tenant,
// defaulting to the caller's own). Only the organization cards follow the selection — the caller's own
// quota and request history never do. A delegated (MSP) caller keeps the member path: the organization
// route never lists a managed tenant's accounts (the header shows them no selector).
const scope = useReportingScope();
const crossTenant = scope.routeGlobal && !scope.isDelegatedScope;
const { effectiveTenantId, isGlobalOverride } = scope;
@@ -212,7 +211,6 @@ export function SectionMcpUsage() {
{upn && <p className="text-sm text-gray-500">{upn}</p>}
</div>
<div className="flex flex-wrap items-center gap-3">
{!scope.isDelegatedScope && <TenantScopeSelector scope={scope} />}
{/* Plan Badge */}
{usagePlan && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">