Files
fleet/frontend/pages/hosts/details/cards/HostHeader/HostHeader.tsx
T
c510e5b6e2 Add tooltip explaining no Refetch button for Android hosts on Host details page (#50666)
Follow up PR for the following quick win based on feedback here:
- https://github.com/fleetdm/fleet/issues/50001
<img width="899" height="217" alt="Screenshot 2026-08-06 at 10 32 24 AM"
src="https://github.com/user-attachments/assets/462df6ae-ec09-433d-831b-9a0e3c242081"
/>

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


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

* **New Features**
* Android host details now display a disabled **Refetch** button with an
explanatory tooltip.
* The tooltip explains automatic synchronization and links to manual
Android synchronization instructions in a new tab.
* The **Last fetched** information is displayed directly without an
additional tooltip.

* **Documentation**
* Updated Android host documentation to reflect the disabled Refetch
control and manual synchronization guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: LeAnn Gove <leann@fleetdm.com>
Co-authored-by: LeAnn <97471894+Leanngove@users.noreply.github.com>
2026-08-07 10:18:24 -07:00

236 lines
6.6 KiB
TypeScript

import React, { useRef } from "react";
import classnames from "classnames";
import { isAndroid, isIPadOrIPhone } from "interfaces/platform";
import Button from "components/buttons/Button";
import Icon from "components/Icon/Icon";
import { HumanTimeDiffWithFleetLaunchCutoff } from "components/HumanTimeDiffWithDateTip";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import { useCheckTruncatedElement } from "hooks/useCheckTruncatedElement";
import TooltipWrapper from "components/TooltipWrapper";
import { MdmEnrollmentStatus } from "interfaces/mdm";
import { HostMdmDeviceStatusUIState } from "../../helpers";
import {
ANDROID_NO_REFETCH_TOOLTIP_MESSAGE,
DEVICE_STATUS_TAGS,
REFETCH_TOOLTIP_MESSAGES,
} from "./helpers";
const baseClass = "host-header";
interface IRefetchButtonProps {
isDisabled: boolean;
isFetching: boolean;
tooltip?: React.ReactNode;
onRefetchHost: (
evt: React.MouseEvent<HTMLButtonElement, React.MouseEvent>
) => void;
}
const RefetchButton = ({
isDisabled,
isFetching,
tooltip,
onRefetchHost,
}: IRefetchButtonProps) => {
const classNames = classnames({
"refetch-spinner": isFetching,
"refetch-btn": !isFetching,
});
const buttonText = isFetching
? "Fetching fresh vitals...this may take a moment"
: "Refetch";
return (
<>
<TooltipWrapper
underline={false}
disableTooltip={!tooltip}
tipContent={tooltip}
position="top"
showArrow
>
<div className={`${baseClass}__refetch`}>
<Button
className={classNames}
disabled={isDisabled || isFetching}
onClick={onRefetchHost}
variant="secondary"
>
<Icon name="refresh" color="ui-fleet-black-75" size="small" />
{buttonText}
</Button>
</div>
</TooltipWrapper>
</>
);
};
interface IHostSummaryProps {
summaryData: any; // TODO: create interfaces for this and use consistently across host pages and related helpers
showRefetchSpinner: boolean;
onRefetchHost: (
evt: React.MouseEvent<HTMLButtonElement, React.MouseEvent>
) => void;
renderActionsDropdown: () => JSX.Element | null;
deviceUser?: boolean;
/** Optional override for the title shown when `deviceUser` is true.
* Falls back to "My device" if not provided. */
deviceUserHeader?: string;
hostMdmDeviceStatus?: HostMdmDeviceStatusUIState;
hostMdmEnrollmentStatus: MdmEnrollmentStatus | null;
}
const HostHeader = ({
summaryData,
showRefetchSpinner,
onRefetchHost,
renderActionsDropdown,
deviceUser,
deviceUserHeader,
hostMdmDeviceStatus,
hostMdmEnrollmentStatus,
}: IHostSummaryProps) => {
const { platform } = summaryData;
const hostDisplayName = useRef<HTMLHeadingElement>(null);
const isTruncated = useCheckTruncatedElement(hostDisplayName);
const renderRefetch = () => {
if (isAndroid(platform)) {
return (
<RefetchButton
isDisabled
isFetching={false}
tooltip={ANDROID_NO_REFETCH_TOOLTIP_MESSAGE}
onRefetchHost={onRefetchHost}
/>
);
}
const isOnline = summaryData.status === "online";
let isDisabled = false;
let tooltip;
// we don't have a concept of "online" for iPads and iPhones
if (!isIPadOrIPhone(platform)) {
// deviceStatus can be `undefined` in the case of the MyDevice Page not sending
// this prop. When this is the case or when it is `unlocked`, we only take
// into account the host being online or offline for correctly render the
// refresh button. If we have a value for deviceStatus, we then need to also
// take it account for rendering the button.
if (
hostMdmDeviceStatus === undefined ||
hostMdmDeviceStatus === "unlocked"
) {
isDisabled = !isOnline;
tooltip = !isOnline ? REFETCH_TOOLTIP_MESSAGES.offline : null;
} else {
isDisabled = true;
tooltip = REFETCH_TOOLTIP_MESSAGES[hostMdmDeviceStatus];
}
} else {
// ios and ipad devices refresh buttons disable state is determined only by the
// host mdm device status.
// eslint-disable-next-line
if (
hostMdmDeviceStatus === undefined ||
hostMdmDeviceStatus === "unlocked" ||
(hostMdmDeviceStatus === "locked" &&
hostMdmEnrollmentStatus === "On (automatic)")
) {
isDisabled = false;
tooltip = null;
} else {
isDisabled = true;
tooltip = REFETCH_TOOLTIP_MESSAGES[hostMdmDeviceStatus];
}
}
return (
<RefetchButton
isDisabled={isDisabled}
isFetching={showRefetchSpinner}
tooltip={tooltip}
onRefetchHost={onRefetchHost}
/>
);
};
const lastFetched = summaryData.detail_updated_at ? (
<HumanTimeDiffWithFleetLaunchCutoff
timeString={summaryData.detail_updated_at}
/>
) : (
": unavailable"
);
const renderDeviceStatusTag = () => {
if (!hostMdmDeviceStatus || hostMdmDeviceStatus === "unlocked") return null;
const tag = DEVICE_STATUS_TAGS[hostMdmDeviceStatus];
const title = tag.title;
const tipContent = tag.generateTooltip(platform);
const classNames = classnames(
`${baseClass}__device-status-tag`,
tag.tagType
);
return (
<>
<TooltipWrapper
tipContent={tipContent}
position="top"
underline={false}
showArrow
className={`${baseClass}__device-status-tag-wrapper`}
>
<span className={classNames}>{title}</span>
</TooltipWrapper>
</>
);
};
return (
<div className={`${baseClass} header title`}>
<div className="title__inner">
<div className="display-name-container">
<TooltipWrapper
disableTooltip={!isTruncated}
tipContent={
deviceUser
? deviceUserHeader || "My device"
: summaryData.display_name || DEFAULT_EMPTY_CELL_VALUE
}
underline={false}
position="top"
showArrow
>
<h1 className="display-name" ref={hostDisplayName}>
{deviceUser
? deviceUserHeader || "My device"
: summaryData.display_name || DEFAULT_EMPTY_CELL_VALUE}
</h1>
</TooltipWrapper>
{renderDeviceStatusTag()}
<div className={`${baseClass}__last-fetched`}>
{"Last fetched"} {lastFetched}
</div>
</div>
</div>
<div className="title__actions">
{renderRefetch()}
{renderActionsDropdown()}
</div>
</div>
);
};
export default HostHeader;