diff --git a/assets/images/icon-apple-black-24x24@2x.png b/assets/images/icon-apple-black-24x24@2x.png new file mode 100644 index 0000000000..50f389ab87 Binary files /dev/null and b/assets/images/icon-apple-black-24x24@2x.png differ diff --git a/assets/images/icon-apple-vibrant-blue-24x24@2x.png b/assets/images/icon-apple-vibrant-blue-24x24@2x.png new file mode 100644 index 0000000000..4dd07cfd50 Binary files /dev/null and b/assets/images/icon-apple-vibrant-blue-24x24@2x.png differ diff --git a/assets/images/icon-circle-check-blue-48x48@2x.png b/assets/images/icon-circle-check-blue-48x48@2x.png new file mode 100644 index 0000000000..67a2b7b533 Binary files /dev/null and b/assets/images/icon-circle-check-blue-48x48@2x.png differ diff --git a/assets/images/icon-linux-black-24x24@2x.png b/assets/images/icon-linux-black-24x24@2x.png new file mode 100644 index 0000000000..93663b4991 Binary files /dev/null and b/assets/images/icon-linux-black-24x24@2x.png differ diff --git a/assets/images/icon-linux-vibrant-blue-24x24@2x.png b/assets/images/icon-linux-vibrant-blue-24x24@2x.png new file mode 100644 index 0000000000..2da7e23c0d Binary files /dev/null and b/assets/images/icon-linux-vibrant-blue-24x24@2x.png differ diff --git a/assets/images/icon-windows-black-24x24@2x.png b/assets/images/icon-windows-black-24x24@2x.png new file mode 100644 index 0000000000..f0aa38c98f Binary files /dev/null and b/assets/images/icon-windows-black-24x24@2x.png differ diff --git a/assets/images/icon-windows-vibrant-blue-24x24@2x.png b/assets/images/icon-windows-vibrant-blue-24x24@2x.png new file mode 100644 index 0000000000..5de3e9280f Binary files /dev/null and b/assets/images/icon-windows-vibrant-blue-24x24@2x.png differ diff --git a/changes/issue-5757-download-orbit-installer-sandbox b/changes/issue-5757-download-orbit-installer-sandbox new file mode 100644 index 0000000000..80fb1c9a86 --- /dev/null +++ b/changes/issue-5757-download-orbit-installer-sandbox @@ -0,0 +1 @@ +- Added Fleet Sandbox UI to download pre-packaged installers \ No newline at end of file diff --git a/docs/Contributing/Testing.md b/docs/Contributing/Testing.md index 2f63c85c7c..42b0ef2d5b 100644 --- a/docs/Contributing/Testing.md +++ b/docs/Contributing/Testing.md @@ -409,7 +409,8 @@ awslocal kinesis get-records --shard-iterator AAAAAAAAAAERtiUrWGI0sq99TtpKnmDu6h Pre-built installers are kept in a blob storage like AWS S3. As part of your your local development there's a [MinIO](https://min.io/) instance running on http://localhost:9000. To test the pre-built installers functionality locally: -1. Build the installers you want using `fleetctl package`. +1. Build the installers you want using `fleetctl package`. Be sure to include the `--insecure` flag + for local testing. 2. Use the [installerstore](../../tools/installerstore/README.md) tool to upload them to your MinIO instance. 3. Configure your fleet server setting `FLEET_PACKAGING_GLOBAL_ENROLL_SECRET` to match your global enroll secret. 4. Set `FLEET_DEMO=1`, as the endpoint to retrieve the installer is only available in the sandbox. @@ -418,4 +419,7 @@ Pre-built installers are kept in a blob storage like AWS S3. As part of your you FLEET_DEMO=1 FLEET_PACKAGING_GLOBAL_ENROLL_SECRET=xyz ./build/fleet serve --dev ``` +Be sure to replace the `FLEET_PACKAGING_GLOBAL_ENROLL_SECRET` value above with the global enroll +secret from the `fleetctl package` command used to build the installers. + MinIO also offers a web interface at http://localhost:9001. Credentials are `minio` / `minio123!`. diff --git a/frontend/components/AddHostsModal/AddHostsModal.tsx b/frontend/components/AddHostsModal/AddHostsModal.tsx index c867539b78..13cad6defe 100644 --- a/frontend/components/AddHostsModal/AddHostsModal.tsx +++ b/frontend/components/AddHostsModal/AddHostsModal.tsx @@ -1,24 +1,52 @@ import React from "react"; +import { ITeamSummary } from "interfaces/team"; +import DataError from "components/DataError"; import Modal from "components/Modal"; -import { ITeam } from "interfaces/team"; -import { IEnrollSecret } from "interfaces/enroll_secret"; +import Spinner from "components/Spinner"; + import PlatformWrapper from "./PlatformWrapper/PlatformWrapper"; +import DownloadInstallers from "./DownloadInstallers/DownloadInstallers"; const baseClass = "add-hosts-modal"; interface IAddHostsModal { + currentTeam?: ITeamSummary; + enrollSecret?: string; + isLoading: boolean; + isSandboxMode?: boolean; onCancel: () => void; - selectedTeam: ITeam | { name: string; secrets: IEnrollSecret[] | null }; } const AddHostsModal = ({ + currentTeam, + enrollSecret, + isLoading, + isSandboxMode, onCancel, - selectedTeam, }: IAddHostsModal): JSX.Element => { + const renderModalContent = () => { + if (isLoading) { + return ; + } + if (!enrollSecret) { + return ; + } + + // TODO: Currently, prepacked installers in Fleet Sandbox use the global enroll secret, + // and Fleet Sandbox runs Fleet Free so the currentTeam check here is an + // additional precaution/reminder to revisit this in connection with future changes. + // See https://github.com/fleetdm/fleet/issues/4970#issuecomment-1187679407. + return isSandboxMode && !currentTeam ? ( + + ) : ( + + ); + }; + return ( - + {renderModalContent()} ); }; diff --git a/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx b/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx new file mode 100644 index 0000000000..316e5d4a85 --- /dev/null +++ b/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx @@ -0,0 +1,185 @@ +import React, { useState } from "react"; +import FileSaver from "file-saver"; + +import { + IInstallerPlatform, + IInstallerType, + INSTALLER_PLATFORM_BY_TYPE, + INSTALLER_TYPE_BY_PLATFORM, +} from "interfaces/installer"; +import installerAPI from "services/entities/installers"; + +import Button from "components/buttons/Button"; +import Checkbox from "components/forms/fields/Checkbox"; +import DataError from "components/DataError"; +import Spinner from "components/Spinner"; +import TooltipWrapper from "components/TooltipWrapper"; + +import AppleIcon from "./../../../../assets/images/icon-apple-black-24x24@2x.png"; +import AppleIconVibrant from "./../../../../assets/images/icon-apple-vibrant-blue-24x24@2x.png"; +import LinuxIcon from "./../../../../assets/images/icon-linux-black-24x24@2x.png"; +import LinuxIconVibrant from "./../../../../assets/images/icon-linux-vibrant-blue-24x24@2x.png"; +import WindowsIcon from "./../../../../assets/images/icon-windows-black-24x24@2x.png"; +import WindowsIconVibrant from "./../../../../assets/images/icon-windows-vibrant-blue-24x24@2x.png"; +import SuccessIcon from "./../../../../assets/images/icon-circle-check-blue-48x48@2x.png"; + +interface IDownloadInstallersProps { + enrollSecret: string; + onCancel: () => void; +} + +const baseClass = "download-installers"; + +const displayOrder = [ + "macOS", + "Windows", + "Linux (RPM)", + "Linux (deb)", +] as const; + +const displayIcon = (platform: IInstallerPlatform, isSelected: boolean) => { + switch (platform) { + case "Linux (RPM)": + case "Linux (deb)": + return ( + {platform} + ); + case "macOS": + return ( + {platform} + ); + case "Windows": + return ( + {platform} + ); + default: + return null; + } +}; + +const DownloadInstallers = ({ + enrollSecret, + onCancel, +}: IDownloadInstallersProps): JSX.Element => { + const [includeDesktop, setIncludeDesktop] = useState(true); + const [isDownloadError, setIsDownloadError] = useState(false); + const [isDownloading, setIsDownloading] = useState(false); + const [isDownloadSuccess, setIsDownloadSuccess] = useState(false); + const [selectedInstaller, setSelectedInstaller] = useState< + IInstallerType | undefined + >(); + + const downloadInstaller = async (installerType?: IInstallerType) => { + if (!installerType) { + // do nothing + return; + } + setIsDownloading(true); + try { + const blob: BlobPart = await installerAPI.downloadInstaller({ + enrollSecret, + installerType, + includeDesktop, + }); + const filename = `fleet-osquery.${installerType}`; + const file = new global.window.File([blob], filename, { + type: "application/octet-stream", + }); + FileSaver.saveAs(file); + setIsDownloadSuccess(true); + } catch { + setIsDownloadError(true); + } finally { + setIsDownloading(false); + } + }; + + const onClickSelector = (type: IInstallerType) => { + if (isDownloading) { + // do nothing + return; + } + if (type === selectedInstaller) { + setSelectedInstaller(undefined); + return; + } + setSelectedInstaller(type); + }; + + if (isDownloadError) { + return ( +
+ +
+ ); + } + + if (isDownloadSuccess) { + const installerPlatform = + (selectedInstaller && + `${INSTALLER_PLATFORM_BY_TYPE[selectedInstaller]} `) || + ""; + return ( +
+ download successful +

You’re almost there

+

{`Run the installer on a ${installerPlatform}laptop, workstation, or sever to add it to Fleet.`}

+ +
+ ); + } + + return ( +
+

Which platform is your host running?

+
+ {displayOrder.map((platform) => { + const installerType = INSTALLER_TYPE_BY_PLATFORM[platform]; + const isSelected = selectedInstaller === installerType; + return ( +
onClickSelector(installerType)} + > + + {displayIcon(platform, isSelected)} + {platform} + +
+ ); + })} +
+ setIncludeDesktop(value)} + value={includeDesktop} + > + <> + Include  + Lightweight application that allows end users to see information about their device.

" + } + > + Fleet Desktop +
+ +
+ +
+ ); +}; + +export default DownloadInstallers; diff --git a/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss b/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss new file mode 100644 index 0000000000..ae45b4a810 --- /dev/null +++ b/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss @@ -0,0 +1,104 @@ +.download-installers { + display: flex; + flex-direction: column; + padding: 0; + padding-bottom: 20px; + + p { + padding-top: $pad-small; + padding-bottom: $pad-medium; + margin: 0; + } + + .component__tooltip-wrapper__tip-text { + p { + padding: 0; + } + } + + .form-field.form-field--checkbox { + padding-bottom: $pad-large; + margin-bottom: 0; + } + + &__select-installer { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; + gap: 8px; + padding-bottom: $pad-large; + } + + &__selector { + cursor: pointer; + font-size: $small; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 16px; + gap: 16px; + width: 286px; + + border: 1px solid #c5c7d1; + border-radius: 4px; + + span { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + img { + height: 24px; + width: auto; + padding-right: 8px; + } + } + + &:hover { + border-color: $core-vibrant-blue; + } + + &--selected { + color: $core-vibrant-blue; + background-color: $ui-vibrant-blue-10; + border-color: $core-vibrant-blue; + } + } + + &__button--download { + width: 154px; + height: 38px; + } + + &__success { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 0 70px; + + img { + height: 48px; + width: 48px; + } + + h2 { + margin: 0; + padding: 16px 0 8px; + } + + p { + margin: 0; + padding-bottom: 24px; + } + } + + &__error { + .data-error__inner { + margin: 0; + padding-bottom: 20px; + } + } +} diff --git a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx index 2560c5408a..63e2e8230f 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx @@ -53,23 +53,21 @@ const platformSubNav: IPlatformSubNav[] = [ }, ]; -interface IPlatformWrapperProp { - selectedTeam: ITeam | { name: string; secrets: IEnrollSecret[] | null }; +interface IPlatformWrapperProps { + enrollSecret: string; onCancel: () => void; } const baseClass = "platform-wrapper"; const PlatformWrapper = ({ - selectedTeam, + enrollSecret, onCancel, -}: IPlatformWrapperProp): JSX.Element => { +}: IPlatformWrapperProps): JSX.Element => { const { config, isPreviewMode } = useContext(AppContext); const { renderFlash } = useContext(NotificationContext); const [copyMessage, setCopyMessage] = useState>({}); - const [includeFleetDesktop, setIncludeFleetDesktop] = useState( - false - ); + const [includeFleetDesktop, setIncludeFleetDesktop] = useState(true); const [showPlainOsquery, setShowPlainOsquery] = useState(false); const { @@ -127,11 +125,6 @@ const PlatformWrapper = ({ --carver_continue_endpoint=/api/v1/osquery/carve/block --carver_block_size=2000000`; - let enrollSecret: string; - if (selectedTeam.secrets) { - enrollSecret = selectedTeam.secrets[0].secret; - } - const onDownloadEnrollSecret = (evt: React.MouseEvent) => { evt.preventDefault(); @@ -407,7 +400,7 @@ const PlatformWrapper = ({ <> setIncludeFleetDesktop(!includeFleetDesktop)} + onChange={(value: boolean) => setIncludeFleetDesktop(value)} value={includeFleetDesktop} > <> diff --git a/frontend/interfaces/installer.ts b/frontend/interfaces/installer.ts new file mode 100644 index 0000000000..341dac572a --- /dev/null +++ b/frontend/interfaces/installer.ts @@ -0,0 +1,27 @@ +export type IInstallerType = "pkg" | "msi" | "rpm" | "deb"; + +export type IInstallerPlatform = + | "Windows" + | "macOS" + | "Linux (RPM)" + | "Linux (deb)"; + +export const INSTALLER_TYPE_BY_PLATFORM: Record< + IInstallerPlatform, + IInstallerType +> = { + macOS: "pkg", + Windows: "msi", + "Linux (RPM)": "rpm", + "Linux (deb)": "deb", +} as const; + +export const INSTALLER_PLATFORM_BY_TYPE: Record< + IInstallerType, + IInstallerPlatform +> = { + pkg: "macOS", + msi: "Windows", + rpm: "Linux (RPM)", + deb: "Linux (deb)", +} as const; diff --git a/frontend/interfaces/team.ts b/frontend/interfaces/team.ts index 6f7f14ed78..20a462ba88 100644 --- a/frontend/interfaces/team.ts +++ b/frontend/interfaces/team.ts @@ -16,7 +16,7 @@ export default PropTypes.shape({ }); /** - * The id, name, and optional description for a team entity + * The id, name, description, and host count for a team entity */ export interface ITeamSummary { id: number; diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx index 5e47b86509..7a4e28be9d 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx @@ -487,11 +487,15 @@ const TeamDetailsWrapper = ({ {showAddHostsModal && ( )} {showManageEnrollSecretsModal && ( diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index e52a9c7d4e..ae8fd3b4dc 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -314,13 +314,6 @@ const ManageHostsPage = ({ } ); - const addHostsTeam = currentTeam - ? { name: currentTeam.name, secrets: teamSecrets || null } - : { - name: "No team", - secrets: globalSecrets || null, - }; - const { data: teams, isLoading: isLoadingTeams, @@ -1187,9 +1180,25 @@ const ManageHostsPage = ({ /> ); - const renderAddHostsModal = () => ( - - ); + const renderAddHostsModal = () => { + const enrollSecret = + // TODO: Currently, prepacked installers in Fleet Sandbox use the global enroll secret, + // and Fleet Sandbox runs Fleet Free so the isSandboxMode check here is an + // additional precaution/reminder to revisit this in connection with future changes. + // See https://github.com/fleetdm/fleet/issues/4970#issuecomment-1187679407. + currentTeam && !isSandboxMode + ? teamSecrets?.[0].secret + : globalSecrets?.[0].secret; + return ( + + ); + }; const renderTransferHostModal = () => { if (!teams) { @@ -1344,7 +1353,9 @@ const ManageHostsPage = ({ isHostCountLoading ? "count-loading" : "" }`} > - {`${count} host${count === 1 ? "" : "s"}`} + {count !== undefined && ( + {`${count} host${count === 1 ? "" : "s"}`} + )} {count ? (