mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
- **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>
262 lines
6.8 KiB
TypeScript
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 > 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'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 <> • {versionInfo}</>;
|
|
};
|
|
|
|
const renderTimeStamp = () =>
|
|
addedTimestamp ? (
|
|
<>
|
|
{" "}
|
|
•{" "}
|
|
<TooltipWrapper
|
|
tipContent={internationalTimeFormat(new Date(addedTimestamp))}
|
|
underline={false}
|
|
>
|
|
{addedFromNow(addedTimestamp)}
|
|
</TooltipWrapper>
|
|
</>
|
|
) : (
|
|
""
|
|
);
|
|
|
|
const renderSha256 = () => {
|
|
return sha256 ? (
|
|
<>
|
|
{" "}
|
|
•{" "}
|
|
<span className={`${baseClass}__sha256`}>
|
|
<TooltipWrapper
|
|
tipContent={<>The software's SHA-256 hash.</>}
|
|
position="top"
|
|
showArrow
|
|
underline={false}
|
|
>
|
|
{sha256.slice(0, 7)}…
|
|
</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;
|