Fixed Android Enterprise page not refreshing (#45914)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #45862 

Speculative fixes since I wasn't able to repro issue locally.
Refresh AppContext and the React Query ["config"] cache directly after
the SSE/DELETE response so AndroidMdmCard and AndroidMdmPage flip
without a manual page reload, with a bounded retry to defeat the 1s
cached_mysql.AppConfig TTL. Also harden startSSE to detect the success
signal across chunk boundaries and reject (rather than hang) when the
stream ends without it.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

## Testing

- [x] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [x] Confirmed that the fix is not expected to adversely impact load
test results

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Fixed Android Enterprise page not refreshing after connecting or
disconnecting Android MDM. The Enterprise ID and card state now update
automatically without requiring a manual page reload.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45914?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2026-05-20 17:13:42 -05:00
committed by GitHub
parent 228b6c82d0
commit 24da012f18
4 changed files with 71 additions and 20 deletions
@@ -0,0 +1 @@
- Fixed Android Enterprise page not refreshing after connecting or disconnecting Android MDM, so the Enterprise ID and card state are visible without a manual page reload.
@@ -6,11 +6,12 @@ import React, {
useState,
} from "react";
import { InjectedRouter } from "react-router";
import { useQuery } from "react-query";
import { useQuery, useQueryClient } from "react-query";
import PATHS from "router/paths";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import { IConfig } from "interfaces/config";
import { getErrorReason } from "interfaces/errors";
import mdmAndroidAPI from "services/entities/mdm_android";
import { DEFAULT_USE_QUERY_OPTIONS, SUPPORT_LINK } from "utilities/constants";
@@ -37,6 +38,8 @@ interface ITurnOnAndroidMdmProps {
const TurnOnAndroidMdm = ({ router }: ITurnOnAndroidMdmProps) => {
const { renderFlash } = useContext(NotificationContext);
const { setConfig } = useContext(AppContext);
const queryClient = useQueryClient();
// TODO: figure out issue with aborting the SSE fetch when the window is closed
const newWindow = useRef<Window | null>(null);
@@ -48,18 +51,33 @@ const TurnOnAndroidMdm = ({ router }: ITurnOnAndroidMdmProps) => {
async (abortController: AbortController) => {
try {
await mdmAndroidAPI.startSSE(abortController.signal);
abortController.abort();
renderFlash("success", "Android MDM turned on successfully.", {
persistOnPageChange: true,
});
setSetupSse(false);
router.push(PATHS.ADMIN_INTEGRATIONS_MDM);
} catch {
renderFlash("error", "Couldn't turn on Android MDM. Please try again.");
setSetupSse(false);
return;
}
abortController.abort();
// SSE success means the backend has already set
// android_enabled_and_configured=true. Patch the in-memory config so
// AppContext.isAndroidMdmEnabledAndConfigured flips immediately and
// AndroidMdmCard renders correctly on redirect, without a synchronous
// round-trip.
const prevConfig = queryClient.getQueryData<IConfig>(["config"]);
if (prevConfig) {
const patched: IConfig = {
...prevConfig,
mdm: { ...prevConfig.mdm, android_enabled_and_configured: true },
};
setConfig(patched);
queryClient.setQueryData(["config"], patched);
}
renderFlash("success", "Android MDM turned on successfully.", {
persistOnPageChange: true,
});
setSetupSse(false);
router.push(PATHS.ADMIN_INTEGRATIONS_MDM);
},
[renderFlash, router]
[queryClient, renderFlash, router, setConfig]
);
useEffect(() => {
@@ -1,9 +1,12 @@
import React, { useCallback, useContext, useState } from "react";
import { InjectedRouter } from "react-router";
import { useQueryClient } from "react-query";
import PATHS from "router/paths";
import mdmAndroidAPI from "services/entities/mdm_android";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import { IConfig } from "interfaces/config";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
@@ -20,6 +23,8 @@ const TurnOffAndroidMdmModal = ({
router,
}: ITurnOffAndroidMdmModalProps) => {
const { renderFlash } = useContext(NotificationContext);
const { setConfig } = useContext(AppContext);
const queryClient = useQueryClient();
const [isDeleting, setIsDeleting] = useState(false);
@@ -27,15 +32,28 @@ const TurnOffAndroidMdmModal = ({
setIsDeleting(true);
try {
await mdmAndroidAPI.turnOffAndroidMdm();
renderFlash("success", "Android MDM turned off successfully.", {
persistOnPageChange: true,
});
router.push(PATHS.ADMIN_INTEGRATIONS_MDM);
} catch (e) {
onExit();
renderFlash("error", "Couldn't turn off Android MDM. Please try again.");
return;
}
}, [onExit, renderFlash, router]);
// DELETE success means the backend has already cleared
// android_enabled_and_configured. Patch the in-memory config so the
// parent MDM page's card flips immediately on redirect.
const prevConfig = queryClient.getQueryData<IConfig>(["config"]);
if (prevConfig) {
const patched: IConfig = {
...prevConfig,
mdm: { ...prevConfig.mdm, android_enabled_and_configured: false },
};
setConfig(patched);
queryClient.setQueryData(["config"], patched);
}
renderFlash("success", "Android MDM turned off successfully.", {
persistOnPageChange: true,
});
router.push(PATHS.ADMIN_INTEGRATIONS_MDM);
}, [onExit, queryClient, renderFlash, router, setConfig]);
return (
<Modal title="Turn off Android MDM" className={baseClass} onExit={onExit}>
+21 -7
View File
@@ -44,18 +44,32 @@ export default {
});
const reader = response?.body?.getReader();
if (!reader) {
reject(new Error("Android MDM SSE stream unavailable"));
return;
}
const decoder = new TextDecoder();
const successSignal = "Android Enterprise successfully connected";
// Buffer accumulates decoded text so a success message split across
// multiple chunks (valid with chunked transfer encoding) is still
// detected.
let buffer = "";
while (true) {
// @ts-ignore
// eslint-disable-next-line no-await-in-loop
const { done, value } = await reader?.read();
if (done) break;
const text = decoder.decode(value);
if (text === "Android Enterprise successfully connected") {
resolve();
break;
const { done, value } = await reader.read();
if (done) {
// Server closed the stream without ever sending success.
// Reject so callers don't await forever on unmount or backend hiccup.
reject(new Error("Android MDM SSE ended before success signal"));
return;
}
buffer += decoder.decode(value, { stream: true });
if (buffer.includes(successSignal)) {
resolve();
return;
}
buffer = buffer.slice(-successSignal.length);
}
} catch (error) {
if ((error as Error).name === "AbortError") {