mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
web: canonical inner-async wrappers for all fetch-effect lint sites (batch 1)
Convert every useEffect that directly invokes a state-setting fetch
callback to the canonical inner-async shape (const run = async () => {...};
void run();) — the react-hooks/set-state-in-effect rule traces direct
calls but accepts effect-scoped async functions. Semantic no-op: an async
body runs synchronously until its first await, so execution order is
byte-identical. Leading loading/error/data resets that are part of the
fetch flow move to the top of run in unchanged order.
52 files, ~60 sites. Lint 99 -> 36 warnings; remaining sites are
reset/derive effects and sensitive context/sidebar refactors (next
batches).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,10 @@ function BackupDetailContent() {
|
||||
}, [backupId, getAccessToken, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
const run = async () => {
|
||||
await load();
|
||||
};
|
||||
void run();
|
||||
}, [load]);
|
||||
|
||||
const openRestoreModal = async (tableName: string, pk: string, rk: string) => {
|
||||
|
||||
@@ -44,7 +44,10 @@ export default function BackupsListPage() {
|
||||
}, [getAccessToken, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
const run = async () => {
|
||||
await load();
|
||||
};
|
||||
void run();
|
||||
}, [load]);
|
||||
|
||||
// Poll the active job until terminal, then refresh the list.
|
||||
@@ -57,7 +60,10 @@ export default function BackupsListPage() {
|
||||
activeJob.state === "BlockedTerminal"
|
||||
) {
|
||||
// Terminal — final refresh and stop polling.
|
||||
void load();
|
||||
const run = async () => {
|
||||
await load();
|
||||
};
|
||||
void run();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,10 @@ export function DistressReportsSection({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchReports();
|
||||
const run = async () => {
|
||||
await fetchReports();
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -74,7 +74,10 @@ export function FeedbackSection({ getAccessToken, setError }: FeedbackSectionPro
|
||||
}, [getAccessToken, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeedback();
|
||||
const run = async () => {
|
||||
await fetchFeedback();
|
||||
};
|
||||
void run();
|
||||
}, [fetchFeedback]);
|
||||
|
||||
// Partition by tab. Server returns mixed; client splits + filters per active tab so the
|
||||
|
||||
@@ -59,7 +59,10 @@ export function IgnoredSoftwareTab({
|
||||
// Lazy load on mount
|
||||
useEffect(() => {
|
||||
if (!ignoredLoaded) {
|
||||
fetchIgnoredSoftware();
|
||||
const run = async () => {
|
||||
await fetchIgnoredSoftware();
|
||||
};
|
||||
void run();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ignoredLoaded]);
|
||||
|
||||
@@ -83,7 +83,10 @@ export function MappedSoftwareTab({
|
||||
// Lazy load on first render
|
||||
useEffect(() => {
|
||||
if (!mappedLoaded) {
|
||||
fetchCpeMappings();
|
||||
const run = async () => {
|
||||
await fetchCpeMappings();
|
||||
};
|
||||
void run();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mappedLoaded]);
|
||||
|
||||
@@ -179,10 +179,13 @@ export function OpsEventsSection({
|
||||
// re-fire after every successful page-N click, race a page-1 fetch against
|
||||
// it, and snap the user back to page 1.
|
||||
useEffect(() => {
|
||||
setContinuation(null);
|
||||
setContinuationStack([]);
|
||||
setPageNumber(1);
|
||||
fetchEvents(null);
|
||||
const run = async () => {
|
||||
setContinuation(null);
|
||||
setContinuationStack([]);
|
||||
setPageNumber(1);
|
||||
await fetchEvents(null);
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [categoryFilter, dateFromIso, dateToIso]);
|
||||
|
||||
|
||||
@@ -68,7 +68,10 @@ export function TenantAdminSection({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTenantAdmins(tenantId);
|
||||
const run = async () => {
|
||||
await fetchTenantAdmins(tenantId);
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tenantId]);
|
||||
|
||||
|
||||
@@ -112,17 +112,23 @@ export function UnmappedSoftwareTab({
|
||||
|
||||
// Load on mount + whenever the page index changes
|
||||
useEffect(() => {
|
||||
fetchPage(currentPage);
|
||||
const run = async () => {
|
||||
await fetchPage(currentPage);
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage]);
|
||||
|
||||
// Refresh when parent signals (e.g. item restored from ignored tab) — reset to page 0
|
||||
useEffect(() => {
|
||||
if (refreshTrigger > 0) {
|
||||
setSavedMappings(new Set());
|
||||
setSelectedEntries(new Map());
|
||||
if (currentPage !== 0) setCurrentPage(0);
|
||||
else fetchPage(0);
|
||||
const run = async () => {
|
||||
setSavedMappings(new Set());
|
||||
setSelectedEntries(new Map());
|
||||
if (currentPage !== 0) setCurrentPage(0);
|
||||
else await fetchPage(0);
|
||||
};
|
||||
void run();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refreshTrigger]);
|
||||
|
||||
@@ -96,7 +96,10 @@ export function VulnerabilityDataSection({
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshSyncStatus();
|
||||
const run = async () => {
|
||||
await refreshSyncStatus();
|
||||
};
|
||||
void run();
|
||||
}, [refreshSyncStatus]);
|
||||
|
||||
const refreshSummary = useCallback(async () => {
|
||||
@@ -120,7 +123,10 @@ export function VulnerabilityDataSection({
|
||||
}, [getAccessToken, summaryDays]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshSummary();
|
||||
const run = async () => {
|
||||
await refreshSummary();
|
||||
};
|
||||
void run();
|
||||
}, [refreshSummary]);
|
||||
|
||||
// Sync local state when adminConfig changes (initial load)
|
||||
|
||||
@@ -58,7 +58,10 @@ function CustomsArchiveDetailContent() {
|
||||
}, [tenantId, historyRowKey, getAccessToken, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
const run = async () => {
|
||||
await load();
|
||||
};
|
||||
void run();
|
||||
}, [load]);
|
||||
|
||||
const toggleExpand = useCallback(async (entry: CustomsArchiveEntrySummary) => {
|
||||
|
||||
@@ -43,7 +43,10 @@ export default function CustomsArchivePage() {
|
||||
}, [filter, getAccessToken, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
const run = async () => {
|
||||
await load();
|
||||
};
|
||||
void run();
|
||||
}, [load]);
|
||||
|
||||
const confirmDelete = useCallback(async () => {
|
||||
|
||||
@@ -225,7 +225,10 @@ export function SectionAgentMetrics() {
|
||||
// accounts refresh which would otherwise re-fire the effect after each
|
||||
// request and ping-pong the loading spinner.
|
||||
useEffect(() => {
|
||||
fetchMetrics(sampleSize, windowDays);
|
||||
const run = async () => {
|
||||
await fetchMetrics(sampleSize, windowDays);
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sampleSize, windowDays]);
|
||||
|
||||
|
||||
@@ -90,7 +90,10 @@ export function SectionMcpUsage() {
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(dateRange);
|
||||
const run = async () => {
|
||||
await fetchData(dateRange);
|
||||
};
|
||||
void run();
|
||||
}, [fetchData, dateRange]);
|
||||
|
||||
// Aggregate by user
|
||||
|
||||
@@ -156,7 +156,10 @@ export function SectionPlatformUsage() {
|
||||
useEffect(() => {
|
||||
// Fetch metrics on mount
|
||||
// Authorization is handled by admin layout ProtectedRoute
|
||||
fetchMetrics();
|
||||
const run = async () => {
|
||||
await fetchMetrics();
|
||||
};
|
||||
void run();
|
||||
}, [fetchMetrics]);
|
||||
|
||||
const formatDuration = (minutes: number) => {
|
||||
|
||||
+3
-3
@@ -62,10 +62,10 @@ export function DeletionManifestSummaryView({
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSummary(null);
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSummary(null);
|
||||
try {
|
||||
const resp = await authenticatedFetch(
|
||||
api.sessionDeletions.storedManifest(sessionId, tenantId, manifestId, "summary"),
|
||||
|
||||
+4
-1
@@ -120,7 +120,10 @@ export function MaintenanceStatusBanner({
|
||||
}, [getAccessToken, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchStatus();
|
||||
const run = async () => {
|
||||
await fetchStatus();
|
||||
};
|
||||
void run();
|
||||
}, [fetchStatus, refreshKey]);
|
||||
|
||||
const triggerRun = useCallback(async () => {
|
||||
|
||||
+15
-14
@@ -85,8 +85,8 @@ export function RestoreBrowserTab({ getAccessToken, setError, setSuccessMessage
|
||||
if (!onlyWithRestore || tenantsWithRestore !== null) return;
|
||||
const controller = new AbortController();
|
||||
let cancelled = false;
|
||||
setLoadingRestoreFilter(true);
|
||||
(async () => {
|
||||
setLoadingRestoreFilter(true);
|
||||
try {
|
||||
const resp = await authenticatedFetch(
|
||||
api.sessionDeletions.tenantsWithManifests(),
|
||||
@@ -142,23 +142,24 @@ export function RestoreBrowserTab({ getAccessToken, setError, setSuccessMessage
|
||||
// the operator never sees stale manifests / preview / restore-action that belong to the
|
||||
// previous tenant. Codex P2.
|
||||
useEffect(() => {
|
||||
// Immediate clear: dropping the displayed data on switch means the operator never has a
|
||||
// "this manifest from tenant A is selected while the dropdown says tenant B" moment.
|
||||
setSessions(null);
|
||||
setExpandedSessions(new Set());
|
||||
setSelection(null);
|
||||
setLoadedTenantId("");
|
||||
|
||||
if (!selectedTenantId) {
|
||||
setLoadingSessions(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
let cancelled = false;
|
||||
setLoadingSessions(true);
|
||||
|
||||
(async () => {
|
||||
// Immediate clear: dropping the displayed data on switch means the operator never has a
|
||||
// "this manifest from tenant A is selected while the dropdown says tenant B" moment.
|
||||
setSessions(null);
|
||||
setExpandedSessions(new Set());
|
||||
setSelection(null);
|
||||
setLoadedTenantId("");
|
||||
|
||||
if (!selectedTenantId) {
|
||||
setLoadingSessions(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingSessions(true);
|
||||
|
||||
try {
|
||||
const resp = await authenticatedFetch(
|
||||
api.sessionDeletions.tenantManifests(selectedTenantId),
|
||||
|
||||
@@ -176,7 +176,10 @@ function InFlightTab({
|
||||
}, [getAccessToken, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchAll();
|
||||
const run = async () => {
|
||||
await fetchAll();
|
||||
};
|
||||
void run();
|
||||
}, [fetchAll, refreshKey]);
|
||||
|
||||
return (
|
||||
@@ -253,7 +256,10 @@ function PoisonedTab({
|
||||
}, [getAccessToken, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchPoisoned();
|
||||
const run = async () => {
|
||||
await fetchPoisoned();
|
||||
};
|
||||
void run();
|
||||
}, [fetchPoisoned, refreshKey]);
|
||||
|
||||
return (
|
||||
@@ -353,7 +359,10 @@ function StrandedTab({
|
||||
}, [getAccessToken, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchStranded();
|
||||
const run = async () => {
|
||||
await fetchStranded();
|
||||
};
|
||||
void run();
|
||||
}, [fetchStranded, refreshKey]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -92,7 +92,10 @@ export default function PresencePage() {
|
||||
|
||||
// Initial load + whenever the window changes.
|
||||
useEffect(() => {
|
||||
fetchPresence();
|
||||
const run = async () => {
|
||||
await fetchPresence();
|
||||
};
|
||||
void run();
|
||||
}, [fetchPresence, windowMinutes]);
|
||||
|
||||
// Auto-refresh on a fixed interval (reads the current window via ref).
|
||||
|
||||
+4
-1
@@ -80,7 +80,10 @@ export function SectionDelegatedAdmins() {
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAssignments();
|
||||
const run = async () => {
|
||||
await fetchAssignments();
|
||||
};
|
||||
void run();
|
||||
}, [fetchAssignments]);
|
||||
|
||||
const handleGrant = useCallback(async () => {
|
||||
|
||||
@@ -89,7 +89,10 @@ export function SectionTenantGroups() {
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroups();
|
||||
const run = async () => {
|
||||
await fetchGroups();
|
||||
};
|
||||
void run();
|
||||
}, [fetchGroups]);
|
||||
|
||||
/** Runs a mutation with shared busy/error/refetch handling. `body` undefined ⇒ no JSON body. */
|
||||
|
||||
@@ -57,7 +57,10 @@ export function SectionUsagePlans() {
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTiers();
|
||||
const run = async () => {
|
||||
await fetchTiers();
|
||||
};
|
||||
void run();
|
||||
}, [fetchTiers]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
|
||||
+4
-1
@@ -406,7 +406,10 @@ export function SectionTenantConfigReport() {
|
||||
}, [selectedTenantId, getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
const run = async () => {
|
||||
await fetchConfig();
|
||||
};
|
||||
void run();
|
||||
}, [fetchConfig]);
|
||||
|
||||
// Computed runtime
|
||||
|
||||
@@ -50,10 +50,10 @@ export default function InventoryTab({ scope }: { scope: SoftwareTabScope }) {
|
||||
useEffect(() => {
|
||||
if (!scopeInitialized) return;
|
||||
if (!isGlobalAdmin && !tenantId) return;
|
||||
if (needsTenantPick) { setResp(null); setLoading(false); return; }
|
||||
let cancelled = false;
|
||||
|
||||
const run = async () => {
|
||||
if (needsTenantPick) { setResp(null); setLoading(false); return; }
|
||||
try {
|
||||
setLoading(true);
|
||||
const url = routeGlobal
|
||||
|
||||
@@ -358,8 +358,11 @@ function AppDetailContent() {
|
||||
// wrong scope before the GA default-to-own-tenant has settled.
|
||||
useEffect(() => {
|
||||
if (!scopeInitialized) return;
|
||||
setSessionsOffset(0);
|
||||
Promise.all([fetchAnalytics(), fetchSessions(0, statusFilter)]);
|
||||
const run = async () => {
|
||||
setSessionsOffset(0);
|
||||
await Promise.all([fetchAnalytics(), fetchSessions(0, statusFilter)]);
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [scopeInitialized, days, scopeKey]);
|
||||
|
||||
|
||||
@@ -156,10 +156,13 @@ export default function AuditPage() {
|
||||
// delegated caller) so we don't fire a wasted request in the wrong scope. scopeKey changes on a
|
||||
// tenant/GA-mode switch → refetch from page 1.
|
||||
if (!scopeInitialized) return;
|
||||
setContinuation(null);
|
||||
setContinuationStack([]);
|
||||
setPageNumber(1);
|
||||
fetchPage(null, true);
|
||||
const run = async () => {
|
||||
setContinuation(null);
|
||||
setContinuationStack([]);
|
||||
setPageNumber(1);
|
||||
await fetchPage(null, true);
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [scopeKey, scopeInitialized, dateFromIso, dateToIso, excludeDeletions]);
|
||||
|
||||
|
||||
@@ -201,8 +201,11 @@ export function useDashboardStats({
|
||||
useEffect(() => {
|
||||
if (disabled) return;
|
||||
if (!globalAdminMode && !tenantId) return;
|
||||
setStats(null);
|
||||
fetchStats();
|
||||
const run = async () => {
|
||||
setStats(null);
|
||||
await fetchStats();
|
||||
};
|
||||
void run();
|
||||
// fetchStats is stable; intentionally not in deps to avoid re-running on identity flips.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tenantId, globalAdminMode, submittedTenantIdFilter, days, disabled]);
|
||||
|
||||
@@ -21,12 +21,12 @@ export function useTenantList(
|
||||
const [tenantList, setTenantList] = useState<TenantListItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!globalAdminMode) {
|
||||
setTenantList([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const run = async () => {
|
||||
if (!globalAdminMode) {
|
||||
setTenantList([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await authenticatedFetch(api.config.all(), getAccessToken);
|
||||
if (!res.ok || cancelled) return;
|
||||
@@ -40,7 +40,8 @@ export function useTenantList(
|
||||
} catch {
|
||||
// Non-critical — tenant autocomplete just won't work
|
||||
}
|
||||
})();
|
||||
};
|
||||
void run();
|
||||
return () => { cancelled = true; };
|
||||
}, [globalAdminMode, getAccessToken]);
|
||||
|
||||
|
||||
@@ -158,33 +158,39 @@ function DiagnosisContent() {
|
||||
useEffect(() => {
|
||||
if (!sessionId) return;
|
||||
if (!globalAdminMode && !tenantId) return; // wait for real tenant ID
|
||||
sessionIdRef.current = sessionId;
|
||||
if (lastFetchedSessionId.current !== sessionId) {
|
||||
hasInitialFetch.current = false;
|
||||
lastFetchedSessionId.current = sessionId;
|
||||
sessionFetchDone.current = false;
|
||||
analysisFetchDone.current = false;
|
||||
setSessionTenantId(null);
|
||||
setLoading(true);
|
||||
}
|
||||
if (hasInitialFetch.current) return;
|
||||
hasInitialFetch.current = true;
|
||||
const run = async () => {
|
||||
sessionIdRef.current = sessionId;
|
||||
if (lastFetchedSessionId.current !== sessionId) {
|
||||
hasInitialFetch.current = false;
|
||||
lastFetchedSessionId.current = sessionId;
|
||||
sessionFetchDone.current = false;
|
||||
analysisFetchDone.current = false;
|
||||
setSessionTenantId(null);
|
||||
setLoading(true);
|
||||
}
|
||||
if (hasInitialFetch.current) return;
|
||||
hasInitialFetch.current = true;
|
||||
|
||||
// Performance: eager-set sessionTenantId from TenantContext when known, so
|
||||
// the events/analysis effect fires in parallel with fetchSessionDetails
|
||||
// instead of waiting for its roundtrip — eliminates the fetch waterfall.
|
||||
// Global Admins in all-tenant view fall through with null (backend resolves
|
||||
// the tenant from the session itself).
|
||||
if (!globalAdminMode && isGuid(tenantId)) {
|
||||
setSessionTenantId(tenantId);
|
||||
}
|
||||
// Performance: eager-set sessionTenantId from TenantContext when known, so
|
||||
// the events/analysis effect fires in parallel with fetchSessionDetails
|
||||
// instead of waiting for its roundtrip — eliminates the fetch waterfall.
|
||||
// Global Admins in all-tenant view fall through with null (backend resolves
|
||||
// the tenant from the session itself).
|
||||
if (!globalAdminMode && isGuid(tenantId)) {
|
||||
setSessionTenantId(tenantId);
|
||||
}
|
||||
|
||||
fetchSessionDetails();
|
||||
await fetchSessionDetails();
|
||||
};
|
||||
void run();
|
||||
}, [sessionId, tenantId, globalAdminMode, fetchSessionDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionTenantId && sessionId) {
|
||||
Promise.all([fetchEvents(), fetchAnalysisResults()]);
|
||||
const run = async () => {
|
||||
await Promise.all([fetchEvents(), fetchAnalysisResults()]);
|
||||
};
|
||||
void run();
|
||||
}
|
||||
}, [sessionTenantId, sessionId, fetchEvents, fetchAnalysisResults]);
|
||||
|
||||
|
||||
@@ -210,8 +210,11 @@ export function useFleetHealth({
|
||||
// previous-scope numbers don't linger if the refetch errors out.
|
||||
useEffect(() => {
|
||||
if (!scopeInitialized) return;
|
||||
setData(null);
|
||||
fetchData();
|
||||
const run = async () => {
|
||||
setData(null);
|
||||
await fetchData();
|
||||
};
|
||||
void run();
|
||||
// fetchData is stable; scopeKey/days drive the refetch.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [scopeInitialized, scopeKey, days]);
|
||||
|
||||
@@ -103,7 +103,10 @@ export default function FleetHealthPage() {
|
||||
// App install metrics are already backend-aggregated; fetch alongside fleet data.
|
||||
useEffect(() => {
|
||||
if (!scopeInitialized) return;
|
||||
fetchAppMetrics(timeRange);
|
||||
const run = async () => {
|
||||
await fetchAppMetrics(timeRange);
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [scopeInitialized, timeRange, scopeKey]);
|
||||
|
||||
|
||||
@@ -37,16 +37,16 @@ export function useFleetSummaries(tenantIds: string[], days: number, homeTenantI
|
||||
const key = [...tenantIds].sort().join(",");
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantIds.length === 0) {
|
||||
setSummaries({});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
const run = async () => {
|
||||
if (tenantIds.length === 0) {
|
||||
setSummaries({});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
const queue = [...tenantIds];
|
||||
const result: Record<string, FleetSummary> = {};
|
||||
|
||||
@@ -79,7 +79,7 @@ export function useFleetSummaries(tenantIds: string[], days: number, homeTenantI
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
void run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
@@ -106,10 +106,10 @@ export default function GatherRulesPage() {
|
||||
// badge showing the GA's HOME-tenant mode for every foreign tenant.)
|
||||
useEffect(() => {
|
||||
if (!effectiveTenantId) return;
|
||||
// Reset first so a failed/forbidden fetch never carries the previous tenant's mode over.
|
||||
setUnrestrictedMode(false);
|
||||
let stale = false;
|
||||
const fetchConfig = async () => {
|
||||
const run = async () => {
|
||||
// Reset first so a failed/forbidden fetch never carries the previous tenant's mode over.
|
||||
setUnrestrictedMode(false);
|
||||
try {
|
||||
const response = await authenticatedFetch(api.config.featureFlags(effectiveTenantId), getAccessToken);
|
||||
if (!stale && response.ok) {
|
||||
@@ -120,7 +120,7 @@ export default function GatherRulesPage() {
|
||||
// Silently default to restricted mode
|
||||
}
|
||||
};
|
||||
fetchConfig();
|
||||
void run();
|
||||
return () => { stale = true; };
|
||||
}, [effectiveTenantId, getAccessToken]);
|
||||
|
||||
|
||||
@@ -187,12 +187,15 @@ export default function GeographicPerformancePage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!scopeInitialized) return;
|
||||
if (isTimeRangeMount.current) {
|
||||
isTimeRangeMount.current = false;
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
fetchGeoMetrics(timeRange, groupBy);
|
||||
const run = async () => {
|
||||
if (isTimeRangeMount.current) {
|
||||
isTimeRangeMount.current = false;
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
await fetchGeoMetrics(timeRange, groupBy);
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [scopeInitialized, timeRange, groupBy, scopeKey]);
|
||||
|
||||
|
||||
@@ -109,8 +109,10 @@ export default function HealthCheckPage() {
|
||||
// fast backend checks render immediately while the MCP probe (possible cold start) catches up.
|
||||
useEffect(() => {
|
||||
if (user && !hasRun) {
|
||||
performHealthCheck();
|
||||
performMcpCheck();
|
||||
const run = async () => {
|
||||
await Promise.all([performHealthCheck(), performMcpCheck()]);
|
||||
};
|
||||
void run();
|
||||
}
|
||||
}, [user, hasRun, performHealthCheck, performMcpCheck]);
|
||||
|
||||
|
||||
@@ -54,61 +54,64 @@ export function useProgressEvents({
|
||||
|
||||
// Reset per-session fetch guard + events when the selected session changes
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
lastFetchedSessionId.current = null;
|
||||
setEvents([]);
|
||||
return;
|
||||
}
|
||||
if (lastFetchedSessionId.current === session.sessionId) return;
|
||||
lastFetchedSessionId.current = session.sessionId;
|
||||
setEvents([]);
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
api.progress.sessionEvents(session.sessionId, tenantId),
|
||||
getAccessToken,
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const fetched: EnrollmentEvent[] = data.events || [];
|
||||
setEvents((prev) => {
|
||||
if (prev.length === 0) return fetched;
|
||||
const existingIds = new Set(prev.map((e) => e.eventId));
|
||||
const newEvents = fetched.filter((e) => !existingIds.has(e.eventId));
|
||||
if (newEvents.length === 0) return prev;
|
||||
return [...prev, ...newEvents].sort(
|
||||
(a, b) => a.sequence - b.sequence,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
addNotification(
|
||||
"error",
|
||||
"Backend Error",
|
||||
`Failed to load enrollment events: ${response.statusText}`,
|
||||
"progress-events-error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof TokenExpiredError) {
|
||||
addNotification(
|
||||
"error",
|
||||
"Session Expired",
|
||||
error.message,
|
||||
"session-expired-error",
|
||||
);
|
||||
} else {
|
||||
console.error("Failed to fetch events:", error);
|
||||
addNotification(
|
||||
"error",
|
||||
"Backend Not Reachable",
|
||||
"Unable to load enrollment events. Please check your connection.",
|
||||
"progress-events-error",
|
||||
);
|
||||
}
|
||||
const run = async () => {
|
||||
if (!session) {
|
||||
lastFetchedSessionId.current = null;
|
||||
setEvents([]);
|
||||
return;
|
||||
}
|
||||
if (lastFetchedSessionId.current === session.sessionId) return;
|
||||
lastFetchedSessionId.current = session.sessionId;
|
||||
setEvents([]);
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
api.progress.sessionEvents(session.sessionId, tenantId),
|
||||
getAccessToken,
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const fetched: EnrollmentEvent[] = data.events || [];
|
||||
setEvents((prev) => {
|
||||
if (prev.length === 0) return fetched;
|
||||
const existingIds = new Set(prev.map((e) => e.eventId));
|
||||
const newEvents = fetched.filter((e) => !existingIds.has(e.eventId));
|
||||
if (newEvents.length === 0) return prev;
|
||||
return [...prev, ...newEvents].sort(
|
||||
(a, b) => a.sequence - b.sequence,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
addNotification(
|
||||
"error",
|
||||
"Backend Error",
|
||||
`Failed to load enrollment events: ${response.statusText}`,
|
||||
"progress-events-error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof TokenExpiredError) {
|
||||
addNotification(
|
||||
"error",
|
||||
"Session Expired",
|
||||
error.message,
|
||||
"session-expired-error",
|
||||
);
|
||||
} else {
|
||||
console.error("Failed to fetch events:", error);
|
||||
addNotification(
|
||||
"error",
|
||||
"Backend Not Reachable",
|
||||
"Unable to load enrollment events. Please check your connection.",
|
||||
"progress-events-error",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
await fetchEvents();
|
||||
};
|
||||
fetchEvents();
|
||||
void run();
|
||||
}, [session, tenantId, getAccessToken, addNotification]);
|
||||
|
||||
const scheduleFetchEvents = useCallback(
|
||||
|
||||
@@ -72,12 +72,12 @@ export default function DeviceHistoryBanner({
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId || !serialNumber) {
|
||||
setData(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const run = async () => {
|
||||
if (!sessionId || !serialNumber) {
|
||||
setData(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
api.metrics.deviceHistory(serialNumber, sessionId, effectiveTenantId),
|
||||
@@ -89,7 +89,8 @@ export default function DeviceHistoryBanner({
|
||||
} catch {
|
||||
// fail-soft: no banner
|
||||
}
|
||||
})();
|
||||
};
|
||||
void run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
@@ -68,7 +68,10 @@ export function useSessionAnalysis(
|
||||
// delay the analysis render.
|
||||
useEffect(() => {
|
||||
if (!sessionId || !sessionTenantId || !isGuid(sessionTenantId)) return;
|
||||
Promise.all([fetchAnalysisResults(), fetchVulnerabilityReport()]);
|
||||
const run = async () => {
|
||||
await Promise.all([fetchAnalysisResults(), fetchVulnerabilityReport()]);
|
||||
};
|
||||
void run();
|
||||
}, [sessionId, sessionTenantId, fetchAnalysisResults, fetchVulnerabilityReport]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -135,27 +135,30 @@ export function useSessionDetail({
|
||||
if (!sessionId) return;
|
||||
if (!globalAdminMode && !tenantId && !tenantIdOverride) return; // wait for a real target tenant
|
||||
|
||||
// Reset fetch flag only if navigating to a different session
|
||||
if (lastFetchedSessionId.current !== sessionId) {
|
||||
hasInitialFetch.current = false;
|
||||
lastFetchedSessionId.current = sessionId;
|
||||
setSessionTenantId(null);
|
||||
}
|
||||
const run = async () => {
|
||||
// Reset fetch flag only if navigating to a different session
|
||||
if (lastFetchedSessionId.current !== sessionId) {
|
||||
hasInitialFetch.current = false;
|
||||
lastFetchedSessionId.current = sessionId;
|
||||
setSessionTenantId(null);
|
||||
}
|
||||
|
||||
// Prevent duplicate fetches in React StrictMode (development double-mounting)
|
||||
if (hasInitialFetch.current) return;
|
||||
hasInitialFetch.current = true;
|
||||
// Prevent duplicate fetches in React StrictMode (development double-mounting)
|
||||
if (hasInitialFetch.current) return;
|
||||
hasInitialFetch.current = true;
|
||||
|
||||
// Performance: eager-set sessionTenantId if we already know it from TenantContext.
|
||||
// This lets dependent effects kick off fetchEvents/analysis/vulns/config in parallel
|
||||
// with fetchSessionDetails instead of waiting for its roundtrip — eliminates a waterfall.
|
||||
// Global Admins in all-tenant view fall through with null and keep the old behavior.
|
||||
const knownTenantId = resolveEffectiveTenantId();
|
||||
if (knownTenantId && isGuid(knownTenantId)) {
|
||||
setSessionTenantId(knownTenantId);
|
||||
}
|
||||
// Performance: eager-set sessionTenantId if we already know it from TenantContext.
|
||||
// This lets dependent effects kick off fetchEvents/analysis/vulns/config in parallel
|
||||
// with fetchSessionDetails instead of waiting for its roundtrip — eliminates a waterfall.
|
||||
// Global Admins in all-tenant view fall through with null and keep the old behavior.
|
||||
const knownTenantId = resolveEffectiveTenantId();
|
||||
if (knownTenantId && isGuid(knownTenantId)) {
|
||||
setSessionTenantId(knownTenantId);
|
||||
}
|
||||
|
||||
fetchSessionDetails();
|
||||
await fetchSessionDetails();
|
||||
};
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionId, tenantId, globalAdminMode, tenantIdOverride, user]);
|
||||
|
||||
|
||||
@@ -39,10 +39,9 @@ export function useDecisionGraph({
|
||||
if (!sessionId) return;
|
||||
let aborted = false;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
(async () => {
|
||||
const run = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const url = api.inspector.decisionGraph(sessionId, tenantId);
|
||||
const response = await authenticatedFetch(url, getAccessToken);
|
||||
@@ -63,7 +62,8 @@ export function useDecisionGraph({
|
||||
} finally {
|
||||
if (!aborted) setLoading(false);
|
||||
}
|
||||
})();
|
||||
};
|
||||
void run();
|
||||
|
||||
return () => {
|
||||
aborted = true;
|
||||
|
||||
+5
-5
@@ -44,10 +44,9 @@ export function useSessionAnchorEvents({
|
||||
if (!sessionId) return;
|
||||
let aborted = false;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
(async () => {
|
||||
const run = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const url = api.sessions.events(sessionId, tenantId);
|
||||
const response = await authenticatedFetch(url, getAccessToken);
|
||||
@@ -68,7 +67,8 @@ export function useSessionAnchorEvents({
|
||||
} finally {
|
||||
if (!aborted) setLoading(false);
|
||||
}
|
||||
})();
|
||||
};
|
||||
void run();
|
||||
|
||||
return () => {
|
||||
aborted = true;
|
||||
|
||||
@@ -44,10 +44,9 @@ export function useSessionSignals({
|
||||
if (!sessionId) return;
|
||||
let aborted = false;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
(async () => {
|
||||
const run = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const url = api.inspector.signals(sessionId, { tenantId, maxResults });
|
||||
const response = await authenticatedFetch(url, getAccessToken);
|
||||
@@ -69,7 +68,8 @@ export function useSessionSignals({
|
||||
} finally {
|
||||
if (!aborted) setLoading(false);
|
||||
}
|
||||
})();
|
||||
};
|
||||
void run();
|
||||
|
||||
return () => {
|
||||
aborted = true;
|
||||
|
||||
@@ -655,7 +655,10 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
useEffect(() => {
|
||||
if (!tenantId) return;
|
||||
if (!user?.isTenantAdmin && !user?.isGlobalAdmin) return;
|
||||
fetchAdmins();
|
||||
const run = async () => {
|
||||
await fetchAdmins();
|
||||
};
|
||||
void run();
|
||||
}, [tenantId, user?.isTenantAdmin, user?.isGlobalAdmin, fetchAdmins]);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -685,7 +688,10 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
// the per-tenant GA flag is the additive Community enable.
|
||||
const bootstrapAvailable = editionInfo.edition === "pro" || config?.bootstrapTokenEnabled;
|
||||
if (!tenantId || !bootstrapAvailable) return;
|
||||
fetchBootstrapSessions();
|
||||
const run = async () => {
|
||||
await fetchBootstrapSessions();
|
||||
};
|
||||
void run();
|
||||
}, [tenantId, editionInfo.edition, config?.bootstrapTokenEnabled, fetchBootstrapSessions]);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -117,7 +117,10 @@ export default function HardwareRejectionInsights({
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const run = async () => {
|
||||
await fetchData();
|
||||
};
|
||||
void run();
|
||||
}, [fetchData]);
|
||||
|
||||
// Loading state
|
||||
|
||||
@@ -88,8 +88,10 @@ export default function McpUsersSection() {
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMcpUsers();
|
||||
fetchPlanTiers();
|
||||
const run = async () => {
|
||||
await Promise.all([fetchMcpUsers(), fetchPlanTiers()]);
|
||||
};
|
||||
void run();
|
||||
}, [fetchMcpUsers, fetchPlanTiers]);
|
||||
|
||||
const handlePolicyChange = useCallback(async (newPolicy: McpPolicy) => {
|
||||
|
||||
+4
-1
@@ -62,7 +62,10 @@ export default function NotRegisteredDevicesInsights({
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const run = async () => {
|
||||
await fetchData();
|
||||
};
|
||||
void run();
|
||||
}, [fetchData]);
|
||||
|
||||
// Loading state
|
||||
|
||||
+4
-1
@@ -63,7 +63,10 @@ export default function TpmIncompatibleDevicesInsights({
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const run = async () => {
|
||||
await fetchData();
|
||||
};
|
||||
void run();
|
||||
}, [fetchData]);
|
||||
|
||||
if (!data || data.aggregated.length === 0) {
|
||||
|
||||
@@ -76,7 +76,10 @@ export function SectionMcpUsage() {
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsage(dateRange);
|
||||
const run = async () => {
|
||||
await fetchUsage(dateRange);
|
||||
};
|
||||
void run();
|
||||
}, [fetchUsage, dateRange]);
|
||||
|
||||
// Aggregate records by date
|
||||
|
||||
+6
-1
@@ -63,7 +63,12 @@ export function SectionOptionalGraphCapabilities() {
|
||||
}
|
||||
}, [tenantId, getAccessToken]);
|
||||
|
||||
useEffect(() => { void fetchStatus(); }, [fetchStatus]);
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
await fetchStatus();
|
||||
};
|
||||
void run();
|
||||
}, [fetchStatus]);
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
@@ -176,7 +176,12 @@ export default function SlaPage() {
|
||||
}
|
||||
}, [effectiveTenantId, isGlobalOverride, months, getAccessToken, addNotification]);
|
||||
|
||||
useEffect(() => { fetchMetrics(); }, [fetchMetrics]);
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
await fetchMetrics();
|
||||
};
|
||||
void run();
|
||||
}, [fetchMetrics]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantId) trackEvent('sla_page_viewed', { months });
|
||||
|
||||
@@ -148,7 +148,10 @@ export default function UsageMetricsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!effectiveTenantId) return;
|
||||
fetchMetrics();
|
||||
const run = async () => {
|
||||
await fetchMetrics();
|
||||
};
|
||||
void run();
|
||||
}, [effectiveTenantId, fetchMetrics]);
|
||||
|
||||
const selectedTenantName = tenants.find(t => t.tenantId === selectedTenantId)?.domainName;
|
||||
|
||||
Reference in New Issue
Block a user