From d178fa4ef309871efe5330d1c3af36a12aa1f973 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 15 Apr 2026 01:18:14 +0200 Subject: [PATCH] consistent switcher everywhere --- .../Metrics/GetGlobalAppMetricsFunction.cs | 6 +- .../GetGlobalGeographicMetricsFunction.cs | 9 +- .../app/fleet-health/page.tsx | 123 ++++++++++++++---- .../app/geographic-performance/page.tsx | 105 ++++++++++++--- .../tenant/sections/SectionSlaTargets.tsx | 2 +- src/Web/autopilot-monitor-web/lib/api.ts | 8 +- 6 files changed, 206 insertions(+), 47 deletions(-) diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalAppMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalAppMetricsFunction.cs index 7c6b386d..2e266576 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalAppMetricsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalAppMetricsFunction.cs @@ -37,9 +37,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics if (!string.IsNullOrEmpty(daysParam) && int.TryParse(daysParam, out var parsedDays) && parsedDays > 0) days = parsedDays; + var tenantIdFilter = query["tenantId"]; + var cutoff = DateTime.UtcNow.AddDays(-days); - var allSummaries = await _metricsRepo.GetAllAppInstallSummariesAsync(); + var allSummaries = !string.IsNullOrWhiteSpace(tenantIdFilter) + ? await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantIdFilter) + : await _metricsRepo.GetAllAppInstallSummariesAsync(); var summaries = allSummaries.Where(s => s.StartedAt >= cutoff).ToList(); var appGroups = summaries.GroupBy(s => s.AppName).Select(g => diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalGeographicMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalGeographicMetricsFunction.cs index 30516afd..68b9a49f 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalGeographicMetricsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalGeographicMetricsFunction.cs @@ -41,10 +41,15 @@ namespace AutopilotMonitor.Functions.Functions.Metrics days = parsedDays; var groupBy = query["groupBy"] ?? "city"; + var tenantIdFilter = query["tenantId"]; var cutoff = DateTime.UtcNow.AddDays(-days); - var sessions = await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1)); - var allSummaries = await _metricsRepo.GetAllAppInstallSummariesAsync(); + var sessions = !string.IsNullOrWhiteSpace(tenantIdFilter) + ? await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1), tenantIdFilter) + : await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1)); + var allSummaries = !string.IsNullOrWhiteSpace(tenantIdFilter) + ? await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantIdFilter) + : await _metricsRepo.GetAllAppInstallSummariesAsync(); var summaries = allSummaries.Where(s => s.StartedAt >= cutoff).ToList(); var result = GetGeographicMetricsFunction.ComputeGeographicMetrics(sessions, summaries, groupBy); diff --git a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx index 806c3159..dc0a0443 100644 --- a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx +++ b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx @@ -33,6 +33,11 @@ interface AppMetricsResponse { topFailingApps: AppMetric[]; } +interface TenantInfo { + tenantId: string; + domainName: string; +} + export default function FleetHealthPage() { const router = useRouter(); @@ -41,44 +46,90 @@ export default function FleetHealthPage() { const [loading, setLoading] = useState(true); const [timeRange, setTimeRange] = useState<"7d" | "30d" | "90d">("7d"); - const hasInitialFetch = useRef(false); const hasJoinedGroup = useRef(false); const isTimeRangeMount = useRef(true); const { on, off, isConnected, joinGroup, leaveGroup } = useSignalR(); const { tenantId } = useTenant(); - const { getAccessToken } = useAuth(); + const { getAccessToken, user } = useAuth(); const { addNotification } = useNotifications(); const { globalAdminMode } = useAdminMode(); - useEffect(() => { - if (!globalAdminMode && !tenantId) return; // wait for real tenant ID - if (hasInitialFetch.current) return; - hasInitialFetch.current = true; - Promise.all([fetchSessions(), fetchAppMetrics(timeRange)]); - }, [tenantId, globalAdminMode]); + // Global admin: tenant selector state. + // Default for GAs is their own tenant (set once tenantId loads), not aggregated — + // gives the standard "view your tenant first, opt-in to aggregated/override". + const [tenants, setTenants] = useState([]); + const [selectedTenantId, setSelectedTenantId] = useState(""); + const [scopeInitialized, setScopeInitialized] = useState(false); + + const isGlobalAdmin = Boolean(globalAdminMode && user?.isGlobalAdmin); + const isAggregatedGlobalView = Boolean(isGlobalAdmin && !selectedTenantId); + const isGlobalOverride = Boolean( + isGlobalAdmin && selectedTenantId && selectedTenantId !== tenantId + ); + const effectiveTenantId = isGlobalAdmin ? selectedTenantId : tenantId; useEffect(() => { + if (!isGlobalAdmin) return; + const loadTenants = async () => { + try { + const response = await authenticatedFetch(api.config.all(), getAccessToken); + if (response.ok) { + const data = await response.json(); + const mapped: TenantInfo[] = data.map((t: { tenantId: string; domainName: string }) => ({ + tenantId: t.tenantId, + domainName: t.domainName || "", + })); + mapped.sort((a, b) => + (a.domainName || a.tenantId).localeCompare(b.domainName || b.tenantId) + ); + setTenants(mapped); + } + } catch (err) { + console.error("Error fetching tenant list:", err); + } + }; + loadTenants(); + }, [isGlobalAdmin, getAccessToken]); + + const selectedTenantName = useMemo( + () => tenants.find((t) => t.tenantId === selectedTenantId)?.domainName, + [tenants, selectedTenantId] + ); + + useEffect(() => { + if (scopeInitialized) return; + if (!tenantId) return; + if (isGlobalAdmin) { + setSelectedTenantId(tenantId); + } + setScopeInitialized(true); + }, [tenantId, isGlobalAdmin, scopeInitialized]); + + useEffect(() => { + if (!scopeInitialized) return; if (isTimeRangeMount.current) { isTimeRangeMount.current = false; - return; } Promise.all([fetchSessions(timeRange), fetchAppMetrics(timeRange)]); - }, [timeRange]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [scopeInitialized, timeRange, selectedTenantId]); useEffect(() => { - if (isConnected && !hasJoinedGroup.current) { - joinGroup(`tenant-${tenantId}`); - hasJoinedGroup.current = true; - } + if (!isConnected) return; + if (!effectiveTenantId) return; // no group in aggregated mode + if (hasJoinedGroup.current) return; + const group = `tenant-${effectiveTenantId}`; + joinGroup(group); + hasJoinedGroup.current = true; return () => { if (hasJoinedGroup.current) { - leaveGroup(`tenant-${tenantId}`); + leaveGroup(group); hasJoinedGroup.current = false; } }; - }, [isConnected, tenantId]); + }, [isConnected, effectiveTenantId]); useEffect(() => { const handleNewSession = (data: { session: Session }) => { @@ -123,8 +174,8 @@ export default function FleetHealthPage() { const fetchSessions = async (range: "7d" | "30d" | "90d" = timeRange) => { try { const days = range === "7d" ? 7 : range === "30d" ? 30 : 90; - const endpoint = globalAdminMode - ? api.globalSessions.list(undefined, days) + const endpoint = isGlobalAdmin + ? api.globalSessions.list(selectedTenantId || undefined, days) : api.sessions.list(tenantId, days); const response = await authenticatedFetch(endpoint, getAccessToken); if (response.ok) { @@ -148,8 +199,8 @@ export default function FleetHealthPage() { const fetchAppMetrics = async (range: "7d" | "30d" | "90d" = timeRange) => { try { const days = range === "7d" ? 7 : range === "30d" ? 30 : 90; - const endpoint = globalAdminMode - ? api.metrics.globalApp(days) + const endpoint = isGlobalAdmin + ? api.metrics.globalApp(days, selectedTenantId || undefined) : api.metrics.app(tenantId, days); const response = await authenticatedFetch(endpoint, getAccessToken); if (response.ok) { @@ -346,13 +397,20 @@ export default function FleetHealthPage() { return (
- {globalAdminMode && ( + {isGlobalAdmin && (
Global Admin View - — aggregating data across all tenants + + —{" "} + {isAggregatedGlobalView + ? "aggregating across all tenants" + : isGlobalOverride + ? `viewing tenant ${selectedTenantName ?? selectedTenantId}` + : "access to all tenants"} +
)}
@@ -378,7 +436,26 @@ export default function FleetHealthPage() {
-
+
+ {isGlobalAdmin && tenants.length > 0 && ( + <> + + + + )} {(["7d", "30d", "90d"] as const).map((range) => (
+ {isGlobalAdmin && tenants.length > 0 && ( +
+ + +
+ )} {/* Time Range Toggle */}
{(["7d", "30d", "90d"] as TimeRange[]).map((range) => ( @@ -266,7 +339,7 @@ export default function GeographicPerformancePage() {
{/* Geo Disabled Warning */} - {geoMetrics && !geoMetrics.geoLocationEnabled && !globalAdminMode && ( + {geoMetrics && !geoMetrics.geoLocationEnabled && !isAggregatedGlobalView && (
diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionSlaTargets.tsx b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionSlaTargets.tsx index 6969c331..0a1389a0 100644 --- a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionSlaTargets.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionSlaTargets.tsx @@ -77,7 +77,7 @@ export function SectionSlaTargets() { placeholder="e.g. 98.0" className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm" /> -

Requires 20+ installs to evaluate

+

Requires 5+ installs per week to evaluate

diff --git a/src/Web/autopilot-monitor-web/lib/api.ts b/src/Web/autopilot-monitor-web/lib/api.ts index 1f9776c2..ee4a0aeb 100644 --- a/src/Web/autopilot-monitor-web/lib/api.ts +++ b/src/Web/autopilot-monitor-web/lib/api.ts @@ -134,12 +134,12 @@ export const api = { `${API_BASE_URL}/api/global/metrics/usage${qs({ tenantId })}`, app: (tenantId: string, days: number) => `${API_BASE_URL}/api/metrics/app${qs({ tenantId, days: String(days) })}`, - globalApp: (days: number) => - `${API_BASE_URL}/api/global/metrics/app${qs({ days: String(days) })}`, + globalApp: (days: number, tenantId?: string) => + `${API_BASE_URL}/api/global/metrics/app${qs({ days: String(days), tenantId })}`, geographic: (tenantId: string, days: number, groupBy: string) => `${API_BASE_URL}/api/metrics/geographic${qs({ tenantId, days: String(days), groupBy })}`, - globalGeographic: (days: number, groupBy: string) => - `${API_BASE_URL}/api/global/metrics/geographic${qs({ days: String(days), groupBy })}`, + globalGeographic: (days: number, groupBy: string, tenantId?: string) => + `${API_BASE_URL}/api/global/metrics/geographic${qs({ days: String(days), groupBy, tenantId })}`, geographicSessions: (tenantId: string, days: number, groupBy: string, locationKey: string) => `${API_BASE_URL}/api/metrics/geographic/sessions${qs({ tenantId, days: String(days), groupBy, locationKey })}`, globalGeographicSessions: (days: number, groupBy: string, locationKey: string) =>