mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Fleet UI: Surface download URL for Fleet-maintained app when adding (#25762)
This commit is contained in:
parent
fcf4f971c9
commit
9b70a2c819
12 changed files with 179 additions and 19 deletions
1
changes/23116-fma-dl-url
Normal file
1
changes/23116-fma-dl-url
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fleet UI: Surfaced download URL for Fleet-maintained app when adding the software to Fleet
|
||||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -463,4 +463,5 @@ export interface IFleetMaintainedAppDetails {
|
|||
install_script: string;
|
||||
post_install_script: string; // TODO: is this needed?
|
||||
uninstall_script: string;
|
||||
url: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./FleetAppDetailsModal";
|
||||
|
|
@ -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>
|
||||
•
|
||||
<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>
|
||||
•
|
||||
<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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@
|
|||
}
|
||||
|
||||
&__fleet-app-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__fleet-app-summary--left {
|
||||
display: flex;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue