Fleet UI: Surface download URL for Fleet-maintained app when adding (#25762)

This commit is contained in:
RachelElysia 2025-01-27 16:23:08 -05:00 committed by GitHub
parent fcf4f971c9
commit 9b70a2c819
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 179 additions and 19 deletions

1
changes/23116-fma-dl-url Normal file
View file

@ -0,0 +1 @@
- Fleet UI: Surfaced download URL for Fleet-maintained app when adding the software to Fleet

View file

@ -290,6 +290,7 @@ const DEFAULT_FLEET_MAINTAINED_APP_DETAILS_MOCK: IFleetMaintainedAppDetails = {
post_install_script: 'echo "Installed"',
uninstall_script:
"#!/bin/sh\n\n# Fleet extracts and saves package IDs\npkg_ids=$PACKAGE_ID",
url: "http://www.testurl1234abcd.com/testapp",
};
export const createMockFleetMaintainedAppDetails = (

View file

@ -1,6 +1,7 @@
.data-set {
font-size: $x-small;
min-width: max-content;
max-width: min-content;
overflow: hidden;
// ff only
@-moz-document url-prefix() {

View file

@ -1,8 +1,6 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import { ScheduledQueryablePlatform } from "interfaces/platform";
import PlatformCell from "./PlatformCell";

View file

@ -463,4 +463,5 @@ export interface IFleetMaintainedAppDetails {
install_script: string;
post_install_script: string; // TODO: is this needed?
uninstall_script: string;
url: string;
}

View file

@ -1,7 +1,8 @@
import React from "react";
import { noop } from "lodash";
import Modal from "components/Modal";
import Spinner from "components/Spinner";
import { noop } from "lodash";
import React from "react";
const baseClass = "add-fleet-app-software-modal";

View file

@ -0,0 +1,51 @@
import React from "react";
import { screen } from "@testing-library/react";
import { noop } from "lodash";
import { createCustomRenderer } from "test/test-utils";
import FleetAppDetailsModal from "./FleetAppDetailsModal";
describe("FleetAppDetailsModal", () => {
const defaultProps = {
name: "Test App",
platform: "macOS",
version: "1.0.0",
url: "https://example.com/app",
onCancel: noop,
};
it("renders modal with correct title", () => {
const render = createCustomRenderer();
render(<FleetAppDetailsModal {...defaultProps} />);
const modalTitle = screen.getByText("Software details");
expect(modalTitle).toBeInTheDocument();
});
it("displays correct app details", () => {
const render = createCustomRenderer();
render(<FleetAppDetailsModal {...defaultProps} />);
expect(screen.getByText("Name")).toBeInTheDocument();
expect(screen.getByText("Test App")).toBeInTheDocument();
expect(screen.getByText("Platform")).toBeInTheDocument();
expect(screen.getByText("macOS")).toBeInTheDocument();
expect(screen.getByText("Version")).toBeInTheDocument();
expect(screen.getByText("1.0.0")).toBeInTheDocument();
expect(screen.getByText("URL")).toBeInTheDocument();
expect(
screen.getAllByText("https://example.com/app").length
).toBeGreaterThan(0); // Tooltip renders text twice causing use of toBeInTheDocument to fail
});
it("does not render URL field when url prop is not provided", () => {
const render = createCustomRenderer();
const propsWithoutUrl = { ...defaultProps, url: undefined };
render(<FleetAppDetailsModal {...propsWithoutUrl} />);
expect(screen.queryByText("URL")).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,57 @@
import React from "react";
import Modal from "components/Modal";
import DataSet from "components/DataSet";
import TooltipWrapper from "components/TooltipWrapper";
import TooltipTruncatedText from "components/TooltipTruncatedText";
import Button from "components/buttons/Button";
const baseClass = "fleet-app-details-modal";
interface IFleetAppDetailsModalProps {
name: string;
platform: string;
version: string;
url?: string;
onCancel: () => void;
}
const TOOLTIP_MESSAGE =
"Fleet downloads the package from the URL and stores it. Hosts download it from Fleet before install.";
const FleetAppDetailsModal = ({
name,
platform,
version,
url,
onCancel,
}: IFleetAppDetailsModalProps) => {
return (
<Modal className={baseClass} title="Software details" onExit={onCancel}>
<>
<div className={`${baseClass}__modal-content`}>
<DataSet title="Name" value={name} />
<DataSet title="Platform" value={platform} />
<DataSet title="Version" value={version} />
{url && (
<DataSet
title={
<TooltipWrapper tipContent={TOOLTIP_MESSAGE}>
URL
</TooltipWrapper>
}
value={<TooltipTruncatedText value={url} />}
/>
)}
</div>
<div className="modal-cta-wrap">
<Button onClick={onCancel} variant="brand">
Done
</Button>
</div>
</>
</Modal>
);
};
export default FleetAppDetailsModal;

View file

@ -0,0 +1,12 @@
.fleet-app-details-modal {
&__modal-content {
display: flex;
column-gap: $pad-xxlarge;
row-gap: $pad-xlarge;
flex-wrap: wrap;
}
.react-tooltip {
min-width: 120px;
}
}

View file

@ -0,0 +1 @@
export { default } from "./FleetAppDetailsModal";

View file

@ -26,12 +26,14 @@ import SidePanelContent from "components/SidePanelContent";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import PremiumFeatureMessage from "components/PremiumFeatureMessage";
import Card from "components/Card";
import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import FleetAppDetailsForm from "./FleetAppDetailsForm";
import { IFleetMaintainedAppFormData } from "./FleetAppDetailsForm/FleetAppDetailsForm";
import AddFleetAppSoftwareModal from "./AddFleetAppSoftwareModal";
import FleetAppDetailsModal from "./FleetAppDetailsModal";
import {
getErrorMessage,
@ -48,35 +50,49 @@ const AUTOMATIC_POLICY_ERROR_MESSAGE =
const baseClass = "fleet-maintained-app-details-page";
interface ISoftwareSummaryProps {
interface IFleetAppSummaryProps {
name: string;
platform: string;
version: string;
onClickShowAppDetails: (event: MouseEvent) => void;
}
const FleetAppSummary = ({
name,
platform,
version,
}: ISoftwareSummaryProps) => {
onClickShowAppDetails,
}: IFleetAppSummaryProps) => {
return (
<Card
className={`${baseClass}__fleet-app-summary`}
borderRadiusSize="medium"
color="gray"
>
<SoftwareIcon name={name} size="medium" />
<div className={`${baseClass}__fleet-app-summary--details`}>
<div className={`${baseClass}__fleet-app-summary--title`}>{name}</div>
<div className={`${baseClass}__fleet-app-summary--info`}>
<div className={`${baseClass}__fleet-app-summary--details--platform`}>
{PLATFORM_DISPLAY_NAMES[platform as Platform]}
</div>
&bull;
<div className={`${baseClass}__fleet-app-summary--details--version`}>
{version}
<div className={`${baseClass}__fleet-app-summary--left`}>
<SoftwareIcon name={name} size="medium" />
<div className={`${baseClass}__fleet-app-summary--details`}>
<div className={`${baseClass}__fleet-app-summary--title`}>{name}</div>
<div className={`${baseClass}__fleet-app-summary--info`}>
<div
className={`${baseClass}__fleet-app-summary--details--platform`}
>
{PLATFORM_DISPLAY_NAMES[platform as Platform]}
</div>
&bull;
<div
className={`${baseClass}__fleet-app-summary--details--version`}
>
{version}
</div>
</div>
</div>
</div>
<div className={`${baseClass}__fleet-app-summary--show-details`}>
<Button variant="text-icon" onClick={onClickShowAppDetails}>
<Icon name="info" /> Show details
</Button>
</div>
</Card>
);
};
@ -123,6 +139,7 @@ const FleetMaintainedAppDetailsPage = ({
showAddFleetAppSoftwareModal,
setShowAddFleetAppSoftwareModal,
] = useState(false);
const [showAppDetailsModal, setShowAppDetailsModal] = useState(false);
const {
data: fleetApp,
@ -159,6 +176,10 @@ const FleetMaintainedAppDetailsPage = ({
setSelectedOsqueryTable(tableName);
};
const onClickShowAppDetails = () => {
setShowAppDetailsModal(true);
};
const backToAddSoftwareUrl = `${
PATHS.SOFTWARE_ADD_FLEET_MAINTAINED
}?${buildQueryStringFromParams({ team_id: teamId })}`;
@ -288,6 +309,7 @@ const FleetMaintainedAppDetailsPage = ({
name={fleetApp.name}
platform={fleetApp.platform}
version={fleetApp.version}
onClickShowAppDetails={onClickShowAppDetails}
/>
<FleetAppDetailsForm
labels={labels || []}
@ -324,6 +346,15 @@ const FleetMaintainedAppDetailsPage = ({
</SidePanelContent>
)}
{showAddFleetAppSoftwareModal && <AddFleetAppSoftwareModal />}
{showAppDetailsModal && fleetApp && (
<FleetAppDetailsModal
name={fleetApp.name}
platform={fleetApp.platform}
version={fleetApp.version}
url={fleetApp.url}
onCancel={() => setShowAppDetailsModal(false)}
/>
)}
</>
);
};

View file

@ -18,6 +18,11 @@
}
&__fleet-app-summary {
display: flex;
justify-content: space-between;
}
&__fleet-app-summary--left {
display: flex;
gap: $pad-medium;
}