fleet/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tsx
Gabriel Hernandez 789b56000f
Add UI for enabling manual agent install of a bootstrap package (#28550)
For #[26070](https://github.com/fleetdm/fleet/issues/26070)

This adds the UI for enabling a manual agent install for a bootstrap
package. This includes:

**The new form option for enabling manual agent install of a bootstrap
package**


![image](https://github.com/user-attachments/assets/5d271136-e41b-4c03-bbd8-09450ded82dc)

**disabling adding install software and run script options when user has
enabled manual agent install**


![image](https://github.com/user-attachments/assets/24e3ce6e-8c8f-4987-91e6-8f3fa721d67b)


![image](https://github.com/user-attachments/assets/41be4090-b97f-4ffb-ad76-001232ccd434)


**improvements to the setup experience content styling. I've created a
`SetupExperienceContentContainer` component to centralise the styles for
the content of these sub sections.**

**updates to the preview sections copy and replacing the gifs with
videos**

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [ ] Added/updated automated tests
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com>
2025-04-29 15:29:21 +01:00

186 lines
5.7 KiB
TypeScript

import React, { useContext, useState } from "react";
import { useQuery } from "react-query";
import { AxiosResponse } from "axios";
import { IBootstrapPackageMetadata } from "interfaces/mdm";
import { IApiError } from "interfaces/errors";
import { IConfig } from "interfaces/config";
import { API_NO_TEAM_ID, ITeamConfig } from "interfaces/team";
import mdmAPI from "services/entities/mdm";
import configAPI from "services/entities/config";
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
import { NotificationContext } from "context/notification";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import Spinner from "components/Spinner";
import SectionHeader from "components/SectionHeader";
import BootstrapPackagePreview from "./components/BootstrapPackagePreview";
import PackageUploader from "./components/BootstrapPackageUploader";
import UploadedPackageView from "./components/UploadedPackageView";
import DeleteBootstrapPackageModal from "./components/DeleteBootstrapPackageModal";
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
import BootstrapAdvancedOptions from "./components/BootstrapAdvancedOptions";
const baseClass = "bootstrap-package";
export const getManualAgentInstallSetting = (
currentTeamId: number,
globalConfig?: IConfig,
teamConfig?: ITeamConfig
) => {
if (currentTeamId === API_NO_TEAM_ID) {
return globalConfig?.mdm.macos_setup.manual_agent_install || false;
}
return teamConfig?.mdm?.macos_setup.manual_agent_install || false;
};
interface IBootstrapPackageProps {
currentTeamId: number;
}
const BootstrapPackage = ({ currentTeamId }: IBootstrapPackageProps) => {
const { renderFlash } = useContext(NotificationContext);
const [
selectedManualAgentInstall,
setSelectedManualAgentInstall,
] = useState<boolean>(false);
const [
showDeleteBootstrapPackageModal,
setShowDeleteBootstrapPackageModal,
] = useState(false);
const {
isLoading: isLoadingGlobalConfig,
refetch: refetchGlobalConfig,
} = useQuery<IConfig, Error>(
["config", currentTeamId],
() => configAPI.loadAll(),
{
...DEFAULT_USE_QUERY_OPTIONS,
enabled: currentTeamId === API_NO_TEAM_ID,
onSuccess: (data) => {
setSelectedManualAgentInstall(
getManualAgentInstallSetting(currentTeamId, data)
);
},
}
);
const {
isLoading: isLoadingTeamConfig,
refetch: refetchTeamConfig,
} = useQuery<ILoadTeamResponse, Error, ITeamConfig>(
["team", currentTeamId],
() => teamsAPI.load(currentTeamId),
{
...DEFAULT_USE_QUERY_OPTIONS,
enabled: currentTeamId !== API_NO_TEAM_ID,
select: (res) => res.team,
onSuccess: (data) => {
setSelectedManualAgentInstall(
getManualAgentInstallSetting(currentTeamId, undefined, data)
);
},
}
);
const {
data: bootstrapMetadata,
isLoading,
error,
refetch: refretchBootstrapMetadata,
} = useQuery<
IBootstrapPackageMetadata,
AxiosResponse<IApiError>,
IBootstrapPackageMetadata
>(
["bootstrap-metadata", currentTeamId],
() => mdmAPI.getBootstrapPackageMetadata(currentTeamId),
{
retry: false,
refetchOnWindowFocus: false,
cacheTime: 0,
}
);
const onUpload = () => {
refretchBootstrapMetadata();
};
const onDelete = async () => {
try {
await mdmAPI.deleteBootstrapPackage(currentTeamId);
await mdmAPI.updateSetupExperienceSettings({
team_id: currentTeamId,
manual_agent_install: false,
});
renderFlash("success", "Successfully deleted!");
} catch {
renderFlash("error", "Couldn't delete. Please try again.");
} finally {
setShowDeleteBootstrapPackageModal(false);
refretchBootstrapMetadata();
if (currentTeamId !== API_NO_TEAM_ID) {
refetchTeamConfig();
} else {
refetchGlobalConfig();
}
}
};
// we are relying on the API to tell us this resource does not exist to
// determine if the user has uploaded a bootstrap package.
const noPackageUploaded =
(error && error.status === 404) || !bootstrapMetadata;
const renderBootstrapView = () => {
const bootstrapPackageView = noPackageUploaded ? (
<PackageUploader currentTeamId={currentTeamId} onUpload={onUpload} />
) : (
<UploadedPackageView
bootstrapPackage={bootstrapMetadata}
currentTeamId={currentTeamId}
onDelete={() => setShowDeleteBootstrapPackageModal(true)}
/>
);
return (
<SetupExperienceContentContainer className={`${baseClass}__content`}>
<div className={`${baseClass}__uploader-container`}>
{bootstrapPackageView}
<BootstrapAdvancedOptions
currentTeamId={currentTeamId}
enableInstallManually={!noPackageUploaded}
selectManualAgentInstall={selectedManualAgentInstall}
onChange={(manualAgentInstall) => {
setSelectedManualAgentInstall(manualAgentInstall);
}}
/>
</div>
<div className={`${baseClass}__preview-container`}>
<BootstrapPackagePreview />
</div>
</SetupExperienceContentContainer>
);
};
return (
<section className={baseClass}>
<SectionHeader title="Bootstrap package" />
{isLoading || isLoadingGlobalConfig || isLoadingTeamConfig ? (
<Spinner />
) : (
renderBootstrapView()
)}
{showDeleteBootstrapPackageModal && (
<DeleteBootstrapPackageModal
onDelete={onDelete}
onCancel={() => setShowDeleteBootstrapPackageModal(false)}
/>
)}
</section>
);
};
export default BootstrapPackage;