fleet/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/ViewYamlModal/ViewYamlModal.tsx
Ian Littman 17ff0968d4
Support providing multiple packages per software package file in GitOps (#32503)
For #30849.

# 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] QA'd all new/changed functionality manually

## New Fleet configuration settings

- [n/a] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [x] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [x] Verified that any relevant UI is disabled when GitOps mode is
enabled
2025-09-05 08:38:00 -05:00

200 lines
5.9 KiB
TypeScript

import React, { useContext } from "react";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
import { getExtensionFromFileName } from "utilities/file/fileUtils";
import FileSaver from "file-saver";
import { ISoftwarePackage } from "interfaces/software";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import InfoBanner from "components/InfoBanner";
import CustomLink from "components/CustomLink";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import Editor from "components/Editor";
import { hyphenateString } from "utilities/strings/stringUtils";
import { createPackageYaml, renderDownloadFilesText } from "./helpers";
const baseClass = "view-yaml-modal";
interface IViewYamlModalProps {
softwareTitleName: string;
softwarePackage: ISoftwarePackage;
onExit: () => void;
}
interface HandleDownloadParams {
evt: React.MouseEvent;
content?: string;
filename: string;
filetype: string;
errorMsg: string;
}
const ViewYamlModal = ({
softwareTitleName,
softwarePackage,
onExit,
}: IViewYamlModalProps) => {
const { renderFlash } = useContext(NotificationContext);
const { config } = useContext(AppContext);
const repositoryUrl = config?.gitops?.repository_url;
const {
name,
version,
url,
hash_sha256: sha256,
pre_install_query: preInstallQuery,
install_script: installScript,
post_install_script: postInstallScript,
uninstall_script: uninstallScript,
} = softwarePackage;
const packageYaml = createPackageYaml({
softwareTitle: softwareTitleName,
packageName: name,
version,
url,
sha256,
preInstallQuery,
installScript,
postInstallScript,
uninstallScript,
});
// Generic download handler
const handleDownload = ({
evt,
content,
filename,
filetype,
errorMsg,
}: HandleDownloadParams) => {
evt.preventDefault();
if (content) {
const file = new window.File([content], filename, { type: filetype });
FileSaver.saveAs(file);
} else {
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.",
});
};
return (
<Modal className={baseClass} title="YAML" onExit={onExit}>
<>
<InfoBanner className={`${baseClass}__info-banner`}>
<p>
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.&nbsp;
<CustomLink
url={`${LEARN_MORE_ABOUT_BASE_LINK}/yaml-packages`}
text="How to use YAML"
newTab
multiline
variant="banner-link"
/>
</p>
</InfoBanner>
{repositoryUrl && (
<p>
First, create the YAML file below and save it to your{" "}
<CustomLink url={repositoryUrl} text="repository" newTab />.
</p>
)}
<p>Make sure you reference the package YAML from your team YAML.</p>
<div className={`${baseClass}__form-fields`}>
<InputField
enableCopy
readOnly
name="filename"
label="Filename"
value={`${hyphenatedSoftwareTitle}.package.yml`}
/>
<Editor label="Contents" value={packageYaml} enableCopy />
</div>
<p>
{renderDownloadFilesText({
preInstallQuery,
installScript,
postInstallScript,
uninstallScript,
onClickPreInstallQuery: preInstallQuery
? onDownloadPreInstallQuery
: undefined,
onClickInstallScript: installScript
? onDownloadInstallScript
: undefined,
onClickPostInstallScript: postInstallScript
? onDownloadPostInstallScript
: undefined,
onClickUninstallScript: uninstallScript
? onDownloadUninstallScript
: undefined,
})}
</p>
<div className="modal-cta-wrap">
<Button onClick={onExit}>Done</Button>
</div>
</>
</Modal>
);
};
export default ViewYamlModal;