fleet/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget/InstallerDetailsWidget.tsx
Jahziel Villasana-Espinoza ac4ec2ff27
FMA version rollback (#40038)
- **Gitops specify FMA rollback version (#39582)**
- **Fleet UI: Show versions options for FMA installers (#39583)**
- **rollback: DB and core implementation (#39650)**

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

# 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] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually

---------

Co-authored-by: Jonathan Katz <44128041+jkatz01@users.noreply.github.com>
Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
Co-authored-by: Carlo DiCelico <carlo@fleetdm.com>
2026-02-24 14:00:32 -05:00

262 lines
6.8 KiB
TypeScript

/** TODO: This component is similar to other UI elements that can
* be abstracted to use a shared base component (e.g. DetailsWidget) */
import React, { useState } from "react";
import classnames from "classnames";
import { stringToClipboard } from "utilities/copy_text";
import { internationalTimeFormat } from "utilities/helpers";
import { addedFromNow } from "utilities/date_format";
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
import { useCheckTruncatedElement } from "hooks/useCheckTruncatedElement";
import { InstallerType } from "interfaces/software";
import Graphic from "components/Graphic";
import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon";
import TooltipWrapper from "components/TooltipWrapper";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import CustomLink from "components/CustomLink";
import AndroidLatestVersionWithTooltip from "components/MDM/AndroidLatestVersionWithTooltip";
const baseClass = "installer-details-widget";
interface IInstallerNameProps {
name: string;
}
const InstallerName = ({ name }: IInstallerNameProps) => {
const titleRef = React.useRef<HTMLDivElement>(null);
const isTruncated = useCheckTruncatedElement(titleRef);
return (
<TooltipWrapper
tipContent={name}
position="top"
underline={false}
disableTooltip={!isTruncated}
showArrow
>
<div ref={titleRef} className={`${baseClass}__title`}>
{name}
</div>
</TooltipWrapper>
);
};
const renderInstallerDisplayText = (
installerType: string,
isFma: boolean,
androidPlayStoreId?: string
) => {
if (installerType === "package") {
return isFma ? "Fleet-maintained" : "Custom package";
}
if (androidPlayStoreId) {
return "Google Play Store";
}
return "App Store (VPP)";
};
interface IInstallerDetailsWidgetProps {
className?: string;
softwareName: string;
installerType: InstallerType;
addedTimestamp?: string;
version?: string | null;
sha256?: string | null;
isFma: boolean;
isLatestFmaVersion?: boolean;
isScriptPackage: boolean;
androidPlayStoreId?: string;
customDetails?: string;
}
const InstallerDetailsWidget = ({
className,
softwareName,
installerType,
addedTimestamp,
sha256,
version,
isFma,
isLatestFmaVersion = false,
isScriptPackage,
androidPlayStoreId,
customDetails,
}: IInstallerDetailsWidgetProps) => {
const classNames = classnames(baseClass, className);
const [copyMessage, setCopyMessage] = useState("");
const onCopySha256 = (evt: React.MouseEvent) => {
evt.preventDefault();
stringToClipboard(sha256)
.then(() => setCopyMessage("Copied!"))
.catch(() => setCopyMessage("Copy failed"));
// Clear message after 1 second
setTimeout(() => setCopyMessage(""), 1000);
return false;
};
const renderIcon = () => {
if (installerType === "app-store") {
if (androidPlayStoreId) {
return <SoftwareIcon name="androidPlayStore" size="medium" />;
}
return <SoftwareIcon name="appleAppStore" size="medium" />;
}
return <Graphic name="file-pkg" />;
};
const renderDetails = () => {
if (customDetails) {
return <>{customDetails}</>;
}
const renderVersionInfo = () => {
if (isScriptPackage) {
return null;
}
let versionInfo = <span>{version}</span>;
if (isFma) {
versionInfo = (
<TooltipWrapper
tipContent={
<span>
You can change the version in <strong>Actions &gt; Edit</strong>{" "}
software.
</span>
}
>
<span>
{version} {isLatestFmaVersion ? "(latest)" : ""}
</span>
</TooltipWrapper>
);
}
if (installerType === "app-store") {
versionInfo = (
<TooltipWrapper tipContent={<span>Updated every hour.</span>}>
<span>{version}</span>
</TooltipWrapper>
);
}
if (!version) {
versionInfo = (
<TooltipWrapper
tipContent={
<span>
Fleet couldn&apos;t read the version from {softwareName}.
{installerType === "package" && (
<>
{" "}
<CustomLink
newTab
url={`${LEARN_MORE_ABOUT_BASE_LINK}/read-package-version`}
text="Learn more"
variant="tooltip-link"
/>
</>
)}
</span>
}
>
<span>Version (unknown)</span>
</TooltipWrapper>
);
}
if (androidPlayStoreId) {
versionInfo = (
<AndroidLatestVersionWithTooltip
androidPlayStoreId={androidPlayStoreId}
/>
);
}
return <> &bull; {versionInfo}</>;
};
const renderTimeStamp = () =>
addedTimestamp ? (
<>
{" "}
&bull;{" "}
<TooltipWrapper
tipContent={internationalTimeFormat(new Date(addedTimestamp))}
underline={false}
>
{addedFromNow(addedTimestamp)}
</TooltipWrapper>
</>
) : (
""
);
const renderSha256 = () => {
return sha256 ? (
<>
{" "}
&bull;{" "}
<span className={`${baseClass}__sha256`}>
<TooltipWrapper
tipContent={<>The software&apos;s SHA-256 hash.</>}
position="top"
showArrow
underline={false}
>
{sha256.slice(0, 7)}&hellip;
</TooltipWrapper>
<div className={`${baseClass}__sha-copy-button`}>
<Button
variant="icon"
size="small"
iconStroke
onClick={onCopySha256}
>
<Icon name="copy" />
</Button>
</div>
<div className={`${baseClass}__copy-overlay`}>
{copyMessage && (
<div
className={`${baseClass}__copy-message`}
>{`${copyMessage} `}</div>
)}
</div>
</span>
</>
) : (
""
);
};
return (
<>
{renderInstallerDisplayText(installerType, isFma, androidPlayStoreId)}
{renderVersionInfo()}
{renderTimeStamp()}
{renderSha256()}
</>
);
};
return (
<div className={classNames}>
{renderIcon()}
<div className={`${baseClass}__info`}>
<InstallerName name={softwareName} />
<div className={`${baseClass}__details`}>{renderDetails()}</div>
</div>
</div>
);
};
export default InstallerDetailsWidget;