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>
236 lines
6.6 KiB
TypeScript
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;
|