diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx index 77174ef397..9a029405c3 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx @@ -17,6 +17,7 @@ import FileProgressModal from "components/FileProgressModal"; import PremiumFeatureMessage from "components/PremiumFeatureMessage"; import Spinner from "components/Spinner"; import DataError from "components/DataError"; +import InfoBanner from "components/InfoBanner"; import CategoriesEndUserExperienceModal from "pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal"; import PackageForm from "pages/SoftwarePage/components/forms/PackageForm"; @@ -170,6 +171,13 @@ const SoftwareCustomPackage = ({ return ( <> + {gitOpsModeEnabled && ( + + Add custom packages in GitOps mode so Fleet can host your software. + After adding, copy its SHA-256 hash into your YAML so the next + GitOps workflow doesn't delete it. + + )} { - const { renderFlash } = useContext(NotificationContext); const { config } = useContext(AppContext); const repositoryUrl = config?.gitops?.repository_url; const { name, version, url, hash_sha256: sha256 } = softwarePackage; - // Script packages (.sh and .ps1) should not expose install_script, - // post_install_script, uninstall_script, or pre_install_query fields - const preInstallQuery = !isScriptPackage - ? softwarePackage.pre_install_query - : undefined; - const installScript = !isScriptPackage - ? softwarePackage.install_script - : undefined; - const postInstallScript = !isScriptPackage - ? softwarePackage.post_install_script - : undefined; - const uninstallScript = !isScriptPackage - ? softwarePackage.uninstall_script - : undefined; - const packageYaml = createPackageYaml({ softwareTitle: softwareTitleName, packageName: name, version, url, sha256, - preInstallQuery, - installScript, - postInstallScript, - uninstallScript, iconUrl: iconUrl || null, displayName, isScriptPackage, }); - // Generic download handler - const handleDownload = async ({ - evt, - content, - downloadUrl, - filename, - filetype, - errorMsg, - }: HandleDownloadParams) => { - evt.preventDefault(); - - try { - if (content) { - const file = new window.File([content], filename, { type: filetype }); - FileSaver.saveAs(file); - } else if (downloadUrl) { - const response = await fetch(downloadUrl); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const blob = await response.blob(); - FileSaver.saveAs(blob, filename); - } else { - throw new Error("No content or URL provided"); - } - } catch (err) { - renderFlash("error", errorMsg); - } - return false; - }; - const hyphenatedSoftwareTitle = hyphenateString(softwareTitleName); - const onDownloadPreInstallQuery = (evt: React.MouseEvent) => { - const softwareExtension = getExtensionFromFileName(name); - const preInstallQueryContent = `- name: "[Pre-install software] ${softwareTitleName} (${softwareExtension})"\n query: ${preInstallQuery}`; - - handleDownload({ - evt, - content: preInstallQueryContent, - filename: `pre-install-query-${hyphenatedSoftwareTitle}.yml`, - filetype: "text/yml", - errorMsg: - "Your pre-install query could not be downloaded. Please create YAML file (.yml) manually.", - }); - }; - - const onDownloadPostInstallScript = (evt: React.MouseEvent) => { - handleDownload({ - evt, - content: postInstallScript, - filename: `post-install-${hyphenatedSoftwareTitle}.sh`, - filetype: "text/sh", - errorMsg: - "Your post-install script could not be downloaded. Please create script file (.sh) manually.", - }); - }; - - const onDownloadInstallScript = (evt: React.MouseEvent) => { - handleDownload({ - evt, - content: installScript, - filename: `install-${hyphenatedSoftwareTitle}.sh`, - filetype: "text/sh", - errorMsg: - "Your install script could not be downloaded. Please create script file (.sh) manually.", - }); - }; - - const onDownloadUninstallScript = (evt: React.MouseEvent) => { - handleDownload({ - evt, - content: uninstallScript, - filename: `uninstall-${hyphenatedSoftwareTitle}.sh`, - filetype: "text/sh", - errorMsg: - "Your uninstall script could not be downloaded. Please create script file (.sh) manually.", - }); - }; - - const onDownloadIcon = async (evt: React.MouseEvent) => { - evt.preventDefault(); - - try { - // Get icon blob + create filename - const response = await softwareAPI.getSoftwareIcon(softwareId, teamId); - // Different from icon's original filename as we are suggesting a standard name used in YAML - const filename = `${hyphenatedSoftwareTitle}-icon.png`; - - // Save the file - FileSaver.saveAs(response.data, filename); - } catch (err) { - renderFlash( - "error", - "Your icon could not be downloaded. Please download the image manually." - ); - } - }; - return ( <> - -

- To complete your GitOps configuration, follow the instructions - below. If the YAML is not added, new installers will be deleted on - the next GitOps run, and edited installers will cause the GitOps run - to fail.  - -

-
{repositoryUrl && (

- First, create the YAML file below and save it to your{" "} - . + Manage in .

)} -

Make sure you reference the package YAML from your fleet YAML.

- + + If you added advanced options, learn how to{" "} + + . + + } + />
-

- {renderDownloadFilesText({ - preInstallQuery, - installScript, - postInstallScript, - uninstallScript, - iconUrl, - onClickPreInstallQuery: preInstallQuery - ? onDownloadPreInstallQuery - : undefined, - onClickInstallScript: installScript - ? onDownloadInstallScript - : undefined, - onClickPostInstallScript: postInstallScript - ? onDownloadPostInstallScript - : undefined, - onClickUninstallScript: uninstallScript - ? onDownloadUninstallScript - : undefined, - onClickIcon: iconUrl ? onDownloadIcon : undefined, - hasAdvancedOptionsAvailable: !isScriptPackage && !isIosOrIpadosApp, - isScriptPackage, - })} -

diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/_styles.scss index 39fa5d8b97..20083d8a6b 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/_styles.scss @@ -1,14 +1,13 @@ .view-yaml-modal { overflow-wrap: anywhere; // Prevent long overflow - .info-banner__info { - p { - margin: 0; // Undo weird top margin from info-banner component - } + .editor__label { + color: $core-fleet-black; } + &__form-fields { display: flex; flex-direction: column; gap: $pad-medium; } -} +} \ No newline at end of file diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tests.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tests.ts index a1e55a5244..a04a7c3f4d 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tests.ts +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tests.ts @@ -1,8 +1,6 @@ -import { render, screen } from "@testing-library/react"; import { createMockSoftwarePackage } from "__mocks__/softwareMock"; -import { noop } from "lodash"; -import { createPackageYaml, renderDownloadFilesText } from "./helpers"; +import createPackageYaml from "./helpers"; describe("createPackageYaml", () => { const { @@ -201,197 +199,3 @@ describe("createPackageYaml", () => { path: ./icons/falcon-sensor-test-package-icon.png`); }); }); - -describe("renderYamlHelperText", () => { - const { - pre_install_query: preInstallQuery, - install_script: installScript, - post_install_script: postInstallScript, - uninstall_script: uninstallScript, - } = createMockSoftwarePackage(); - - it("renders nothing if no scripts/queries are present", () => { - // Empty to simulate 'no items' - const { container } = render(renderDownloadFilesText({})); - expect(container).toBeEmptyDOMElement(); - }); - - it("renders correctly with one item", () => { - // Only install_script present - render( - renderDownloadFilesText({ installScript, onClickInstallScript: noop }) - ); - expect( - screen.getByRole("button", { name: "install script" }) - ).toBeInTheDocument(); - expect( - screen.getByText((content) => - content.includes("add it to your repository using the path above.") - ) - ).toBeInTheDocument(); - expect(screen.queryByText("and")).not.toBeInTheDocument(); - }); - - it("renders correctly with two items", () => { - const { container } = render( - renderDownloadFilesText({ - installScript, - uninstallScript, - onClickInstallScript: noop, - onClickUninstallScript: noop, - }) - ); - expect( - screen.getByRole("button", { name: "install script" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "uninstall script" }) - ).toBeInTheDocument(); - - // In "Next," and "Advanced options," only - const text = container.textContent ?? ""; - const commaCount = (text.match(/,/g) || []).length; - expect(commaCount).toBe(2); - - // No oxford comma for two items - expect( - screen.queryByText((content) => content.includes(", and")) - ).not.toBeInTheDocument(); - expect( - screen.getByText((content) => - content.includes("add them to your repository using the paths above.") - ) - ).toBeInTheDocument(); - }); - - it("renders correctly with all items", () => { - // All present (default) - const { container } = render( - renderDownloadFilesText({ - preInstallQuery, - installScript, - uninstallScript, - postInstallScript, - onClickPreInstallQuery: noop, - onClickInstallScript: noop, - onClickUninstallScript: noop, - onClickPostInstallScript: noop, - }) - ); - expect( - screen.getByRole("button", { name: "pre-install query" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "install script" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "post-install script" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "uninstall script" }) - ).toBeInTheDocument(); - - // In "Next," "Advanced options," and 3 more commas - const text = container.textContent ?? ""; - const commaCount = (text.match(/,/g) || []).length; - expect(commaCount).toBe(5); - - // Oxford comma for four items - expect( - screen.queryByText((content) => content.includes(", and")) - ).toBeInTheDocument(); - expect( - screen.getByText((content) => - content.includes("add them to your repository using the paths above.") - ) - ).toBeInTheDocument(); - }); - - it("renders comma correctly for three items (with Oxford comma)", () => { - // pre_install_query, install_script, uninstall_script present - const { container } = render( - renderDownloadFilesText({ - preInstallQuery, - installScript, - uninstallScript, - onClickPreInstallQuery: noop, - onClickInstallScript: noop, - onClickUninstallScript: noop, - }) - ); - - expect( - screen.getByRole("button", { name: "pre-install query" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "install script" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "uninstall script" }) - ).toBeInTheDocument(); - - // In "Next," "Advanced options," and 2 more commas - const text = container.textContent ?? ""; - const commaCount = (text.match(/,/g) || []).length; - expect(commaCount).toBe(4); - - // Oxford comma for three items - expect( - screen.getByText((content) => content.includes(", and")) - ).toBeInTheDocument(); - expect( - screen.getByText((content) => - content.includes("add them to your repository using the paths above.") - ) - ).toBeInTheDocument(); - }); - - it("does not render advanced options help text when hasAdvancedOptionsAvailable is false", () => { - render( - renderDownloadFilesText({ - installScript: "echo install", - onClickInstallScript: noop, - hasAdvancedOptionsAvailable: false, - }) - ); - - // Should not show the "Advanced options" instructional text - expect(screen.queryByText(/If you edited/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/Advanced options/i)).not.toBeInTheDocument(); - }); - - it("does not render script-only fields for script packages", () => { - // Script packages should not show download links for install_script, - // post_install_script, uninstall_script, or pre_install_query - const { container } = render( - renderDownloadFilesText({ - preInstallQuery, - installScript, - postInstallScript, - uninstallScript, - onClickPreInstallQuery: noop, - onClickInstallScript: noop, - onClickPostInstallScript: noop, - onClickUninstallScript: noop, - isScriptPackage: true, - }) - ); - - // Should not render any download buttons for script-only fields - expect( - screen.queryByRole("button", { name: "pre-install query" }) - ).not.toBeInTheDocument(); - expect( - screen.queryByRole("button", { name: "install script" }) - ).not.toBeInTheDocument(); - expect( - screen.queryByRole("button", { name: "post-install script" }) - ).not.toBeInTheDocument(); - expect( - screen.queryByRole("button", { name: "uninstall script" }) - ).not.toBeInTheDocument(); - - // Container should be empty since no items should be rendered - expect(container).toBeEmptyDOMElement(); - }); -}); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tsx index 62b26b859c..8060a9b8e7 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/helpers.tsx @@ -1,23 +1,7 @@ import React, { MouseEvent } from "react"; -import Button from "components/buttons/Button"; import { hyphenateString } from "utilities/strings/stringUtils"; -interface RenderYamlHelperText { - installScript?: string; - uninstallScript?: string; - preInstallQuery?: string; - postInstallScript?: string; - iconUrl?: string | null; - onClickPreInstallQuery?: (evt: MouseEvent) => void; - onClickInstallScript?: (evt: MouseEvent) => void; - onClickPostInstallScript?: (evt: MouseEvent) => void; - onClickUninstallScript?: (evt: MouseEvent) => void; - onClickIcon?: (evt: MouseEvent) => void; - hasAdvancedOptionsAvailable?: boolean; - isScriptPackage?: boolean; -} - // Helper to join items with commas and Oxford comma before "and" const joinWithCommasAnd = ( elements: { key: string; element: JSX.Element }[] @@ -39,106 +23,6 @@ const joinWithCommasAnd = ( }); }; -export const renderDownloadFilesText = ({ - preInstallQuery, - installScript, - postInstallScript, - uninstallScript, - iconUrl, - onClickPreInstallQuery, - onClickInstallScript, - onClickPostInstallScript, - onClickUninstallScript, - onClickIcon, - hasAdvancedOptionsAvailable = true, - isScriptPackage = false, -}: RenderYamlHelperText): JSX.Element => { - const items: { key: string; element: JSX.Element }[] = []; - - // Script packages (.sh and .ps1) should not expose install_script, - // post_install_script, uninstall_script, or pre_install_query fields - if (!isScriptPackage && preInstallQuery) { - items.push({ - key: "pre-install-query", - element: ( - - ), - }); - } - if (!isScriptPackage && installScript) { - items.push({ - key: "install-script", - element: ( - - ), - }); - } - if (!isScriptPackage && uninstallScript) { - items.push({ - key: "uninstall-script", - element: ( - - ), - }); - } - if (!isScriptPackage && postInstallScript) { - items.push({ - key: "post-install-script", - element: ( - - ), - }); - } - if (iconUrl) { - items.push({ - key: "icon-url", - element: ( - - ), - }); - } - - if (items.length === 0) return <>; - - return ( - <> - Next, download your {joinWithCommasAnd(items)} and add{" "} - {items.length === 1 ? "it" : "them"} to your repository using the{" "} - {items.length === 1 ? "path" : "paths"} above. - {hasAdvancedOptionsAvailable && ( - <> - {" "} - If you edited Advanced options, download and replace the{" "} - {items.length === 1 ? "file" : "files"} in your repository with the - updated {items.length === 1 ? "one" : "ones"}. - - )} - - ); -}; - interface CreatePackageYamlParams { softwareTitle: string; packageName: string; @@ -154,7 +38,7 @@ interface CreatePackageYamlParams { isScriptPackage?: boolean; } -export const createPackageYaml = ({ +const createPackageYaml = ({ softwareTitle, packageName, version, @@ -224,3 +108,5 @@ export const createPackageYaml = ({ return yaml.trim(); }; + +export default createPackageYaml;