mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
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**  **disabling adding install software and run script options when user has enabled manual agent install**   **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>
This commit is contained in:
parent
774fb699d3
commit
789b56000f
69 changed files with 653 additions and 316 deletions
BIN
assets/bootstrap-package-preview@6ee62ccd9121c4b1cf36.mp4
Normal file
BIN
assets/bootstrap-package-preview@6ee62ccd9121c4b1cf36.mp4
Normal file
Binary file not shown.
BIN
assets/end-user-auth-preview@15cd5aa8fe3da2239746.mp4
Normal file
BIN
assets/end-user-auth-preview@15cd5aa8fe3da2239746.mp4
Normal file
Binary file not shown.
BIN
assets/end-user-auth@15cd5aa8fe3da2239746.mp4
Normal file
BIN
assets/end-user-auth@15cd5aa8fe3da2239746.mp4
Normal file
Binary file not shown.
BIN
assets/images/run-script-preview.png
Normal file
BIN
assets/images/run-script-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 332 KiB |
BIN
assets/install-software-preview@2d61227ef199d9c8c951.mp4
Normal file
BIN
assets/install-software-preview@2d61227ef199d9c8c951.mp4
Normal file
Binary file not shown.
BIN
assets/setup-assistant-preview@eae6928943e8ca5bd028.mp4
Normal file
BIN
assets/setup-assistant-preview@eae6928943e8ca5bd028.mp4
Normal file
Binary file not shown.
BIN
assets/videos/bootstrap-package-preview.mp4
Normal file
BIN
assets/videos/bootstrap-package-preview.mp4
Normal file
Binary file not shown.
BIN
assets/videos/end-user-auth-preview.mp4
Normal file
BIN
assets/videos/end-user-auth-preview.mp4
Normal file
Binary file not shown.
BIN
assets/videos/install-software-preview.mp4
Normal file
BIN
assets/videos/install-software-preview.mp4
Normal file
Binary file not shown.
BIN
assets/videos/setup-assistant-preview.mp4
Normal file
BIN
assets/videos/setup-assistant-preview.mp4
Normal file
Binary file not shown.
1
changes/issue-26070-add-ui-for-manual-agent-install
Normal file
1
changes/issue-26070-add-ui-for-manual-agent-install
Normal file
|
|
@ -0,0 +1 @@
|
|||
- add UI for the manual agent install of a bootstrap package
|
||||
|
|
@ -27,6 +27,7 @@ const DEFAULT_CONFIG_MDM_MOCK: IMdmConfig = {
|
|||
},
|
||||
macos_setup: {
|
||||
bootstrap_package: "",
|
||||
manual_agent_install: false,
|
||||
enable_end_user_authentication: false,
|
||||
macos_setup_assistant: null,
|
||||
enable_release_device_manually: false,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Icon from "components/Icon";
|
|||
import Graphic from "components/Graphic";
|
||||
import FileDetails from "components/FileDetails";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
||||
const baseClass = "file-uploader";
|
||||
|
||||
|
|
@ -48,6 +49,9 @@ interface IFileUploaderProps {
|
|||
* @default "button"
|
||||
*/
|
||||
buttonType?: "button" | "link";
|
||||
/** renders a tooltip for the button. If `gitopsCompatible` is set to `true`
|
||||
* this tooltip will not be rendered if gitops mode is enabled. */
|
||||
buttonTooltip?: React.ReactNode;
|
||||
onFileUpload: (files: FileList | null) => void;
|
||||
/** renders the current file with the edit pencil button */
|
||||
canEdit?: boolean;
|
||||
|
|
@ -74,6 +78,7 @@ export const FileUploader = ({
|
|||
className,
|
||||
buttonMessage = "Upload",
|
||||
buttonType = "button",
|
||||
buttonTooltip,
|
||||
onFileUpload,
|
||||
canEdit = false,
|
||||
fileDetails,
|
||||
|
|
@ -121,18 +126,22 @@ export const FileUploader = ({
|
|||
));
|
||||
};
|
||||
|
||||
const renderFileUploader = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__graphics`}>{renderGraphics()}</div>
|
||||
<p className={`${baseClass}__message`}>{message}</p>
|
||||
{additionalInfo && (
|
||||
<p className={`${baseClass}__additional-info`}>{additionalInfo}</p>
|
||||
)}
|
||||
{gitopsCompatible ? (
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={8}
|
||||
renderChildren={(disableChildren) => (
|
||||
const renderUploadButton = () => {
|
||||
// the gitops mode tooltip wrapper takes presedence over other button
|
||||
// renderings
|
||||
if (gitopsCompatible) {
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={8}
|
||||
renderChildren={(disableChildren) => (
|
||||
<TooltipWrapper
|
||||
className={`${baseClass}__manual-install-tooltip`}
|
||||
tipContent={buttonTooltip}
|
||||
disableTooltip={disableChildren || !buttonTooltip}
|
||||
position="top"
|
||||
showArrow
|
||||
underline={false}
|
||||
>
|
||||
<Button
|
||||
className={`${baseClass}__upload-button`}
|
||||
variant={buttonVariant}
|
||||
|
|
@ -146,23 +155,47 @@ export const FileUploader = ({
|
|||
<span>{buttonMessage}</span>
|
||||
</label>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
className={`${baseClass}__upload-button`}
|
||||
variant={buttonVariant}
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
customOnKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<label htmlFor="upload-file">
|
||||
{buttonType === "link" && <Icon name="upload" />}
|
||||
<span>{buttonMessage}</span>
|
||||
</label>
|
||||
</Button>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipWrapper
|
||||
className={`${baseClass}__upload-button`}
|
||||
position="top"
|
||||
tipContent={buttonTooltip}
|
||||
underline={false}
|
||||
showArrow
|
||||
disableTooltip={!buttonTooltip}
|
||||
>
|
||||
<Button
|
||||
className={`${baseClass}__upload-button`}
|
||||
variant={buttonVariant}
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
customOnKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<label htmlFor="upload-file">
|
||||
{buttonType === "link" && <Icon name="upload" />}
|
||||
<span>{buttonMessage}</span>
|
||||
</label>
|
||||
</Button>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFileUploader = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__graphics`}>{renderGraphics()}</div>
|
||||
<p className={`${baseClass}__message`}>{message}</p>
|
||||
{additionalInfo && (
|
||||
<p className={`${baseClass}__additional-info`}>{additionalInfo}</p>
|
||||
)}
|
||||
{renderUploadButton()}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
accept={accept}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ interface IDataTableProps {
|
|||
selectedDropdownFilter?: string;
|
||||
/** Set to true to persist the row selections across table data filters */
|
||||
persistSelectedRows?: boolean;
|
||||
/** Set to `true` to not display the footer section of the table */
|
||||
hideFooter?: boolean;
|
||||
onSelectSingleRow?: (value: Row) => void;
|
||||
onClickRow?: (value: any) => void;
|
||||
onResultsCountChange?: (value: number) => void;
|
||||
|
|
@ -110,6 +112,7 @@ const DataTable = ({
|
|||
searchQueryColumn,
|
||||
selectedDropdownFilter,
|
||||
persistSelectedRows = false,
|
||||
hideFooter = false,
|
||||
onSelectSingleRow,
|
||||
onClickRow,
|
||||
onResultsCountChange,
|
||||
|
|
@ -575,34 +578,36 @@ const DataTable = ({
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={`${baseClass}__footer`}>
|
||||
{renderTableHelpText && !!rows?.length && (
|
||||
<div className={`${baseClass}__table-help-text`}>
|
||||
{renderTableHelpText()}
|
||||
</div>
|
||||
)}
|
||||
{isClientSidePagination ? (
|
||||
<Pagination
|
||||
disablePrev={!canPreviousPage}
|
||||
disableNext={!canNextPage}
|
||||
onPrevPage={() => {
|
||||
toggleAllRowsSelected(false); // Resets row selection on pagination (client-side)
|
||||
onClientSidePaginationChange &&
|
||||
onClientSidePaginationChange(pageIndex - 1);
|
||||
previousPage();
|
||||
}}
|
||||
onNextPage={() => {
|
||||
toggleAllRowsSelected(false); // Resets row selection on pagination (client-side)
|
||||
onClientSidePaginationChange &&
|
||||
onClientSidePaginationChange(pageIndex + 1);
|
||||
nextPage();
|
||||
}}
|
||||
hidePagination={!canPreviousPage && !canNextPage}
|
||||
/>
|
||||
) : (
|
||||
renderPagination && renderPagination()
|
||||
)}
|
||||
</div>
|
||||
{!hideFooter && (
|
||||
<div className={`${baseClass}__footer`}>
|
||||
{renderTableHelpText && !!rows?.length && (
|
||||
<div className={`${baseClass}__table-help-text`}>
|
||||
{renderTableHelpText()}
|
||||
</div>
|
||||
)}
|
||||
{isClientSidePagination ? (
|
||||
<Pagination
|
||||
disablePrev={!canPreviousPage}
|
||||
disableNext={!canNextPage}
|
||||
onPrevPage={() => {
|
||||
toggleAllRowsSelected(false); // Resets row selection on pagination (client-side)
|
||||
onClientSidePaginationChange &&
|
||||
onClientSidePaginationChange(pageIndex - 1);
|
||||
previousPage();
|
||||
}}
|
||||
onNextPage={() => {
|
||||
toggleAllRowsSelected(false); // Resets row selection on pagination (client-side)
|
||||
onClientSidePaginationChange &&
|
||||
onClientSidePaginationChange(pageIndex + 1);
|
||||
nextPage();
|
||||
}}
|
||||
hidePagination={!canPreviousPage && !canNextPage}
|
||||
/>
|
||||
) : (
|
||||
renderPagination && renderPagination()
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ interface ITableContainerProps<T = any> {
|
|||
disableTableHeader?: boolean;
|
||||
/** Set to true to persist the row selections across table data filters */
|
||||
persistSelectedRows?: boolean;
|
||||
/** Set to `true` to not display the footer section of the table */
|
||||
hideFooter?: boolean;
|
||||
/** handler called when the `clear selection` button is called */
|
||||
onClearSelection?: () => void;
|
||||
}
|
||||
|
|
@ -161,6 +163,7 @@ const TableContainer = <T,>({
|
|||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
selectedDropdownFilter,
|
||||
searchQueryColumn,
|
||||
hideFooter,
|
||||
onQueryChange,
|
||||
customControl,
|
||||
customFiltersButton,
|
||||
|
|
@ -558,6 +561,7 @@ const TableContainer = <T,>({
|
|||
setExportRows={setExportRows}
|
||||
onClearSelection={onClearSelection}
|
||||
persistSelectedRows={persistSelectedRows}
|
||||
hideFooter={hideFooter}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export interface IMdmConfig {
|
|||
enable_end_user_authentication: boolean;
|
||||
macos_setup_assistant: string | null;
|
||||
enable_release_device_manually: boolean | null;
|
||||
manual_agent_install: boolean | null;
|
||||
};
|
||||
macos_migration: IMacOsMigrationSettings;
|
||||
windows_updates: {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export interface ITeam extends ITeamSummary {
|
|||
enable_end_user_authentication: boolean;
|
||||
macos_setup_assistant: string | null;
|
||||
enable_release_device_manually: boolean | null;
|
||||
manual_agent_install: boolean | null;
|
||||
};
|
||||
windows_updates: {
|
||||
deadline_days: number | null;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import EndUserAuthentication from "./cards/EndUserAuthentication/EndUserAuthenti
|
|||
import BootstrapPackage from "./cards/BootstrapPackage";
|
||||
import SetupAssistant from "./cards/SetupAssistant";
|
||||
import InstallSoftware from "./cards/InstallSoftware";
|
||||
import SetupExperienceScript from "./cards/SetupExperienceScript";
|
||||
import RunScript from "./cards/RunScript";
|
||||
|
||||
interface ISetupExperienceCardProps {
|
||||
currentTeamId?: number;
|
||||
|
|
@ -23,28 +23,28 @@ const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem<
|
|||
Card: EndUserAuthentication,
|
||||
},
|
||||
{
|
||||
title: "2. Setup assistant",
|
||||
urlSection: "setup-assistant",
|
||||
path: PATHS.CONTROLS_SETUP_ASSITANT,
|
||||
Card: SetupAssistant,
|
||||
},
|
||||
{
|
||||
title: "3. Bootstrap package",
|
||||
title: "2. Bootstrap package",
|
||||
urlSection: "bootstrap-package",
|
||||
path: PATHS.CONTROLS_BOOTSTRAP_PACKAGE,
|
||||
Card: BootstrapPackage,
|
||||
},
|
||||
{
|
||||
title: "4. Install software",
|
||||
title: "3. Install software",
|
||||
urlSection: "install-software",
|
||||
path: PATHS.CONTROLS_INSTALL_SOFTWARE,
|
||||
Card: InstallSoftware,
|
||||
},
|
||||
{
|
||||
title: "5. Run script",
|
||||
title: "4. Run script",
|
||||
urlSection: "run-script",
|
||||
path: PATHS.CONTROLS_RUN_SCRIPT,
|
||||
Card: SetupExperienceScript,
|
||||
Card: RunScript,
|
||||
},
|
||||
{
|
||||
title: "5. Setup assistant",
|
||||
urlSection: "setup-assistant",
|
||||
path: PATHS.CONTROLS_SETUP_ASSITANT,
|
||||
Card: SetupAssistant,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,13 @@ 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";
|
||||
|
|
@ -13,17 +18,72 @@ import SectionHeader from "components/SectionHeader";
|
|||
import BootstrapPackagePreview from "./components/BootstrapPackagePreview";
|
||||
import PackageUploader from "./components/BootstrapPackageUploader";
|
||||
import UploadedPackageView from "./components/UploadedPackageView";
|
||||
import DeletePackageModal from "./components/DeletePackageModal";
|
||||
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 [showDeletePackageModal, setShowDeletePackageModal] = useState(false);
|
||||
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,
|
||||
|
|
@ -51,12 +111,21 @@ const BootstrapPackage = ({ currentTeamId }: IBootstrapPackageProps) => {
|
|||
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.");
|
||||
renderFlash("error", "Couldn't delete. Please try again.");
|
||||
} finally {
|
||||
setShowDeletePackageModal(false);
|
||||
setShowDeleteBootstrapPackageModal(false);
|
||||
refretchBootstrapMetadata();
|
||||
if (currentTeamId !== API_NO_TEAM_ID) {
|
||||
refetchTeamConfig();
|
||||
} else {
|
||||
refetchGlobalConfig();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -65,44 +134,52 @@ const BootstrapPackage = ({ currentTeamId }: IBootstrapPackageProps) => {
|
|||
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 (
|
||||
<div className={baseClass}>
|
||||
<section className={baseClass}>
|
||||
<SectionHeader title="Bootstrap package" />
|
||||
{isLoading ? (
|
||||
{isLoading || isLoadingGlobalConfig || isLoadingTeamConfig ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={`${baseClass}__content`}>
|
||||
{noPackageUploaded ? (
|
||||
<>
|
||||
<PackageUploader
|
||||
currentTeamId={currentTeamId}
|
||||
onUpload={onUpload}
|
||||
/>
|
||||
<div className={`${baseClass}__preview-container`}>
|
||||
<BootstrapPackagePreview />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UploadedPackageView
|
||||
bootstrapPackage={bootstrapMetadata}
|
||||
currentTeamId={currentTeamId}
|
||||
onDelete={() => setShowDeletePackageModal(true)}
|
||||
/>
|
||||
<div className={`${baseClass}__preview-container`}>
|
||||
<BootstrapPackagePreview />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
renderBootstrapView()
|
||||
)}
|
||||
{showDeletePackageModal && (
|
||||
<DeletePackageModal
|
||||
{showDeleteBootstrapPackageModal && (
|
||||
<DeleteBootstrapPackageModal
|
||||
onDelete={onDelete}
|
||||
onCancel={() => setShowDeletePackageModal(false)}
|
||||
onCancel={() => setShowDeleteBootstrapPackageModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
.bootstrap-package {
|
||||
&__content {
|
||||
max-width: $break-xxl;
|
||||
margin: 0 auto;
|
||||
&__uploader-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: $pad-xxlarge;
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
&__content {
|
||||
flex-direction: column;
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: $pad-large;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "bootstrap-advanced-options";
|
||||
|
||||
interface IBootstrapAdvancedOptionsProps {
|
||||
currentTeamId: number;
|
||||
enableInstallManually: boolean;
|
||||
selectManualAgentInstall: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const BootstrapAdvancedOptions = ({
|
||||
currentTeamId,
|
||||
enableInstallManually,
|
||||
selectManualAgentInstall,
|
||||
onChange,
|
||||
}: IBootstrapAdvancedOptionsProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await mdmAPI.updateSetupExperienceSettings({
|
||||
team_id: currentTeamId,
|
||||
manual_agent_install: selectManualAgentInstall,
|
||||
});
|
||||
renderFlash("success", "Successfully updated.");
|
||||
} catch {
|
||||
renderFlash("error", "Something went wrong. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const tooltip = (
|
||||
<>
|
||||
Use this option if you're deploying a custom fleetd via bootstrap
|
||||
package. If enabled, Fleet won't install fleetd automatically. To use
|
||||
this option upload a bootstrap package first.
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<RevealButton
|
||||
className={`${baseClass}__accordion-title`}
|
||||
isShowing={showAdvancedOptions}
|
||||
showText="Show advanced options"
|
||||
hideText="Hide advanced options"
|
||||
caretPosition="after"
|
||||
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<form onSubmit={onSubmit}>
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(gitopsDisable) => (
|
||||
<div className={`${baseClass}__advanced-options-controls`}>
|
||||
<Checkbox
|
||||
value={selectManualAgentInstall}
|
||||
onChange={onChange}
|
||||
disabled={gitopsDisable || !enableInstallManually}
|
||||
>
|
||||
<TooltipWrapper
|
||||
tipContent={tooltip}
|
||||
disableTooltip={gitopsDisable}
|
||||
>
|
||||
Install Fleet's agent (fleetd) manually
|
||||
</TooltipWrapper>
|
||||
</Checkbox>
|
||||
<Button
|
||||
disabled={gitopsDisable || !enableInstallManually}
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BootstrapAdvancedOptions;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
.bootstrap-advanced-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-large;
|
||||
|
||||
&__accordion-title {
|
||||
// use this so we dont center the text.
|
||||
justify-content: normal;
|
||||
}
|
||||
|
||||
&__advanced-options-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-large;
|
||||
|
||||
button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./BootstrapAdvancedOptions";
|
||||
|
|
@ -8,6 +8,7 @@ import endpoints from "utilities/endpoints";
|
|||
import Icon from "components/Icon";
|
||||
import Button from "components/buttons/Button";
|
||||
import Graphic from "components/Graphic";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "bootstrap-package-list-item";
|
||||
|
||||
|
|
@ -82,13 +83,18 @@ const BootstrapPackageListItem = ({
|
|||
url={url}
|
||||
token={bootstrapPackage.token}
|
||||
/>
|
||||
<Button
|
||||
className={`${baseClass}__list-item-button`}
|
||||
variant="text-icon"
|
||||
onClick={() => onDelete(bootstrapPackage)}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(disabled) => (
|
||||
<Button
|
||||
className={`${baseClass}__list-item-button`}
|
||||
variant="text-icon"
|
||||
disabled={disabled}
|
||||
onClick={() => onDelete(bootstrapPackage)}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
|
||||
import OsSetupPreview from "../../../../../../../../assets/images/os-setup-preview.gif";
|
||||
import BootstrapPackageEndUserPreview from "../../../../../../../../assets/videos/bootstrap-package-preview.mp4";
|
||||
|
||||
const baseClass = "bootstrap-package-preview";
|
||||
|
||||
|
|
@ -9,20 +9,17 @@ const BootstrapPackagePreview = () => {
|
|||
<div className={baseClass}>
|
||||
<h3>End user experience</h3>
|
||||
<p>
|
||||
The bootstrap package is automatically installed after the end user
|
||||
authenticates and agrees to the EULA during the <b>Remote Management</b>{" "}
|
||||
screen in macOS Setup Assistant.
|
||||
The bootstrap package is installed after the end user authenticates and
|
||||
agrees to the EULA.
|
||||
</p>
|
||||
<p>
|
||||
The end user is allowed to continue to the next setup screen before the
|
||||
installation starts.
|
||||
</p>
|
||||
<p>The package isn't installed on hosts that already enrolled.</p>
|
||||
<img
|
||||
className={`${baseClass}__preview-img`}
|
||||
src={OsSetupPreview}
|
||||
alt="End user experience during the macOS setup assistant with the
|
||||
bootstrap package installation"
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video
|
||||
className={`${baseClass}__preview-video`}
|
||||
src={BootstrapPackageEndUserPreview}
|
||||
controls
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
font-weight: normal;
|
||||
}
|
||||
|
||||
&__preview-img {
|
||||
&__preview-video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: $pad-xxlarge auto 0;
|
||||
|
|
|
|||
|
|
@ -43,28 +43,28 @@ const BootstrapPackageTable = ({
|
|||
if (isError) return <DataError />;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
columnConfigs={COLUMN_CONFIGS}
|
||||
data={tableData}
|
||||
resultsTitle=""
|
||||
isLoading={isLoading}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
defaultSortHeader={DEFAULT_SORT_HEADER}
|
||||
defaultSortDirection={DEFAULT_SORT_DIRECTION}
|
||||
disableTableHeader
|
||||
disablePagination
|
||||
disableCount
|
||||
emptyComponent={() => (
|
||||
<EmptyTable
|
||||
header="No bootstrap package status"
|
||||
info="Expecting to status data? Try again in a few seconds as the system
|
||||
<TableContainer
|
||||
className={baseClass}
|
||||
columnConfigs={COLUMN_CONFIGS}
|
||||
data={tableData}
|
||||
resultsTitle=""
|
||||
isLoading={isLoading}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
defaultSortHeader={DEFAULT_SORT_HEADER}
|
||||
defaultSortDirection={DEFAULT_SORT_DIRECTION}
|
||||
disableTableHeader
|
||||
disablePagination
|
||||
disableCount
|
||||
hideFooter
|
||||
emptyComponent={() => (
|
||||
<EmptyTable
|
||||
header="No bootstrap package status"
|
||||
info="Expecting to status data? Try again in a few seconds as the system
|
||||
catches up."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
.bootstrap-package-table {
|
||||
padding: $pad-xxlarge;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
border-radius: $border-radius;
|
||||
// keep a minimum height to prevent UI jumping when the table data is done
|
||||
// loading.
|
||||
min-height: 191px;
|
||||
|
||||
.data-table-block .data-table tbody td .w250 {
|
||||
min-width: auto;
|
||||
// some data table overrides to display the table correctly
|
||||
.data-table-block .data-table {
|
||||
tbody td .w250 {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $break-lg) {
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import React from "react";
|
|||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
interface DeletePackageModalProps {
|
||||
interface DeleteBootstrapPackageModalProps {
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "delete-package-modal";
|
||||
const baseClass = "delete-bootstrap-package-modal";
|
||||
|
||||
const DeletePackageModal = ({
|
||||
const DeleteBootstrapPackageModal = ({
|
||||
onCancel,
|
||||
onDelete,
|
||||
}: DeletePackageModalProps) => {
|
||||
}: DeleteBootstrapPackageModalProps) => {
|
||||
return (
|
||||
<Modal
|
||||
className={baseClass}
|
||||
|
|
@ -22,10 +22,14 @@ const DeletePackageModal = ({
|
|||
onEnter={() => onDelete()}
|
||||
>
|
||||
<>
|
||||
<p>Delete the bootstrap package to upload a new one.</p>
|
||||
<p>
|
||||
If you need to remove the package from macOS hosts already enrolled,
|
||||
use your configuration management tool (ex. Munki, Chef, or Puppet).
|
||||
Package won't be uninstalled from existing macOS hosts. Installs
|
||||
or uninstalls currently running on a host will still complete.
|
||||
</p>
|
||||
<p>
|
||||
Option to install Fleet's agent (fleetd) manually will be
|
||||
disabled, so agent will be installed automatically during automatic
|
||||
enollment of macOS hosts.
|
||||
</p>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button type="button" onClick={() => onDelete()} variant="alert">
|
||||
|
|
@ -40,4 +44,4 @@ const DeletePackageModal = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default DeletePackageModal;
|
||||
export default DeleteBootstrapPackageModal;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./DeleteBootstrapPackageModal";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./DeletePackageModal";
|
||||
|
|
@ -8,8 +8,4 @@
|
|||
font-size: $x-small;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upload-list__list-item {
|
||||
border-top: 1px solid $ui-fleet-black-10;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import Spinner from "components/Spinner";
|
|||
import RequireEndUserAuth from "./components/RequireEndUserAuth/RequireEndUserAuth";
|
||||
import EndUserAuthForm from "./components/EndUserAuthForm/EndUserAuthForm";
|
||||
import EndUserExperiencePreview from "./components/EndUserExperiencePreview";
|
||||
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
|
||||
|
||||
const baseClass = "end-user-authentication";
|
||||
|
||||
|
|
@ -83,12 +84,12 @@ const EndUserAuthentication = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<section className={baseClass}>
|
||||
<SectionHeader title="End user authentication" />
|
||||
{isLoadingGlobalConfig || isLoadingTeamConfig ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={`${baseClass}__content`}>
|
||||
<SetupExperienceContentContainer>
|
||||
{!globalConfig || !isIdPConfigured(globalConfig.mdm) ? (
|
||||
<RequireEndUserAuth onClickConnect={onClickConnect} />
|
||||
) : (
|
||||
|
|
@ -98,9 +99,9 @@ const EndUserAuthentication = ({
|
|||
/>
|
||||
)}
|
||||
<EndUserExperiencePreview />
|
||||
</div>
|
||||
</SetupExperienceContentContainer>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
.end-user-authentication {
|
||||
&__content {
|
||||
max-width: $break-xxl;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: $pad-xxlarge;
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
&__content {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
import OsSetupPreview from "../../../../../../../../assets/images/os-setup-preview.gif";
|
||||
import EndUserAuthEndUserPreview from "../../../../../../../../assets/videos/end-user-auth-preview.mp4";
|
||||
|
||||
const baseClass = "end-user-experience-preview";
|
||||
|
||||
|
|
@ -18,19 +18,18 @@ const EndUserExperiencePreview = ({
|
|||
<div className={classes}>
|
||||
<h3>End user experience</h3>
|
||||
<p>
|
||||
When the end user reaches the <b>Remote Management</b> screen in the
|
||||
macOS Setup Assistant, they are asked to authenticate and agree to the
|
||||
end user license agreement (EULA).
|
||||
When the end user reaches the <b>Remote Management</b> screen, they are
|
||||
first asked to authenticate and agree to the end user license agreement
|
||||
(EULA).
|
||||
</p>
|
||||
<p>
|
||||
After, Fleet enrolls the Mac, applies macOS settings, and installs the
|
||||
bootstrap package.
|
||||
</p>
|
||||
<img
|
||||
className={`${baseClass}__preview-img`}
|
||||
src={OsSetupPreview}
|
||||
alt="End user experience during the macOS setup assistant with the user
|
||||
logging in with their IdP provider"
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video
|
||||
className={`${baseClass}__preview-video`}
|
||||
src={EndUserAuthEndUserPreview}
|
||||
controls
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
font-weight: normal;
|
||||
}
|
||||
|
||||
&__preview-img {
|
||||
&__preview-video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: $pad-xxlarge auto 0;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ import { AxiosError } from "axios";
|
|||
import mdmAPI, {
|
||||
IGetSetupExperienceSoftwareResponse,
|
||||
} from "services/entities/mdm";
|
||||
import configAPI from "services/entities/config";
|
||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
||||
import { ISoftwareTitle } from "interfaces/software";
|
||||
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { API_NO_TEAM_ID, ITeamConfig } from "interfaces/team";
|
||||
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
import DataError from "components/DataError";
|
||||
|
|
@ -15,6 +19,8 @@ import Spinner from "components/Spinner";
|
|||
import InstallSoftwarePreview from "./components/InstallSoftwarePreview";
|
||||
import AddInstallSoftware from "./components/AddInstallSoftware";
|
||||
import SelectSoftwareModal from "./components/SelectSoftwareModal";
|
||||
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
|
||||
import { getManualAgentInstallSetting } from "../BootstrapPackage/BootstrapPackage";
|
||||
|
||||
const baseClass = "install-software";
|
||||
|
||||
|
|
@ -51,13 +57,37 @@ const InstallSoftware = ({ currentTeamId }: IInstallSoftwareProps) => {
|
|||
}
|
||||
);
|
||||
|
||||
const { data: globalConfig, isLoading: isLoadingGlobalConfig } = useQuery<
|
||||
IConfig,
|
||||
Error
|
||||
>(["config", currentTeamId], () => configAPI.loadAll(), {
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
enabled: currentTeamId === API_NO_TEAM_ID,
|
||||
});
|
||||
|
||||
const { data: teamConfig, isLoading: isLoadingTeamConfig } = useQuery<
|
||||
ILoadTeamResponse,
|
||||
Error,
|
||||
ITeamConfig
|
||||
>(["team", currentTeamId], () => teamsAPI.load(currentTeamId), {
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
enabled: currentTeamId !== API_NO_TEAM_ID,
|
||||
select: (res) => res.team,
|
||||
});
|
||||
|
||||
const onSave = async () => {
|
||||
setShowSelectSoftwareModal(false);
|
||||
refetchSoftwareTitles();
|
||||
};
|
||||
|
||||
const hasManualAgentInstall = getManualAgentInstallSetting(
|
||||
currentTeamId,
|
||||
globalConfig,
|
||||
teamConfig
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
if (isLoading) {
|
||||
if (isLoading || isLoadingGlobalConfig || isLoadingTeamConfig) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
|
@ -67,14 +97,15 @@ const InstallSoftware = ({ currentTeamId }: IInstallSoftwareProps) => {
|
|||
|
||||
if (softwareTitles || softwareTitles === null) {
|
||||
return (
|
||||
<div className={`${baseClass}__content`}>
|
||||
<SetupExperienceContentContainer>
|
||||
<AddInstallSoftware
|
||||
currentTeamId={currentTeamId}
|
||||
hasManualAgentInstall={hasManualAgentInstall}
|
||||
softwareTitles={softwareTitles}
|
||||
onAddSoftware={() => setShowSelectSoftwareModal(true)}
|
||||
/>
|
||||
<InstallSoftwarePreview />
|
||||
</div>
|
||||
</SetupExperienceContentContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +113,7 @@ const InstallSoftware = ({ currentTeamId }: IInstallSoftwareProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<section className={baseClass}>
|
||||
<SectionHeader title="Install software" />
|
||||
<>{renderContent()}</>
|
||||
{showSelectSoftwareModal && softwareTitles && (
|
||||
|
|
@ -93,7 +124,7 @@ const InstallSoftware = ({ currentTeamId }: IInstallSoftwareProps) => {
|
|||
onExit={() => setShowSelectSoftwareModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
.install-software {
|
||||
&__content {
|
||||
max-width: $break-xxl;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: $pad-xxlarge;
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
&__content {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ describe("AddInstallSoftware", () => {
|
|||
currentTeamId={1}
|
||||
softwareTitles={null}
|
||||
onAddSoftware={noop}
|
||||
hasManualAgentInstall={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ describe("AddInstallSoftware", () => {
|
|||
currentTeamId={1}
|
||||
softwareTitles={[createMockSoftwareTitle(), createMockSoftwareTitle()]}
|
||||
onAddSoftware={noop}
|
||||
hasManualAgentInstall={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -56,6 +58,7 @@ describe("AddInstallSoftware", () => {
|
|||
createMockSoftwareTitle(),
|
||||
]}
|
||||
onAddSoftware={noop}
|
||||
hasManualAgentInstall={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,17 +9,20 @@ import CustomLink from "components/CustomLink";
|
|||
import { ISoftwareTitle } from "interfaces/software";
|
||||
import LinkWithContext from "components/LinkWithContext";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "add-install-software";
|
||||
|
||||
interface IAddInstallSoftwareProps {
|
||||
currentTeamId: number;
|
||||
hasManualAgentInstall: boolean;
|
||||
softwareTitles: ISoftwareTitle[] | null;
|
||||
onAddSoftware: () => void;
|
||||
}
|
||||
|
||||
const AddInstallSoftware = ({
|
||||
currentTeamId,
|
||||
hasManualAgentInstall,
|
||||
softwareTitles,
|
||||
onAddSoftware,
|
||||
}: IAddInstallSoftwareProps) => {
|
||||
|
|
@ -79,6 +82,13 @@ const AddInstallSoftware = ({
|
|||
|
||||
const addedText = getAddedText();
|
||||
const buttonText = getButtonText();
|
||||
const manuallyInstallTooltipText = (
|
||||
<>
|
||||
Disabled because you manually install Fleet's agent (
|
||||
<b>Bootstrap package {">"} Advanced options</b>). Use your bootstrap
|
||||
package to install software during the setup experience.
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
|
|
@ -94,13 +104,26 @@ const AddInstallSoftware = ({
|
|||
</div>
|
||||
<span className={`${baseClass}__added-text`}>{addedText}</span>
|
||||
<div>
|
||||
<Button
|
||||
className={`${baseClass}__button`}
|
||||
onClick={onAddSoftware}
|
||||
disabled={hasNoSoftware}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(disableChildren) => (
|
||||
<TooltipWrapper
|
||||
className={`${baseClass}__manual-install-tooltip`}
|
||||
tipContent={manuallyInstallTooltipText}
|
||||
disableTooltip={disableChildren || !hasManualAgentInstall}
|
||||
position="top"
|
||||
showArrow
|
||||
underline={false}
|
||||
>
|
||||
<Button
|
||||
className={`${baseClass}__button`}
|
||||
onClick={onAddSoftware}
|
||||
disabled={disableChildren || hasManualAgentInstall}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,4 +14,8 @@
|
|||
&__added-text {
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
&__manual-install-tooltip {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
|
||||
import Card from "components/Card";
|
||||
|
||||
import InstallSoftwarePreviewImg from "../../../../../../../../assets/images/install-software-preview.png";
|
||||
import InstallSoftwareEndUserPreview from "../../../../../../../../assets/videos/install-software-preview.mp4";
|
||||
|
||||
const baseClass = "install-software-preview";
|
||||
|
||||
|
|
@ -11,19 +11,22 @@ const InstallSoftwarePreview = () => {
|
|||
<Card color="grey" paddingSize="xxlarge" className={baseClass}>
|
||||
<h3>End user experience</h3>
|
||||
<p>
|
||||
After the <b>Remote Management</b> screen, the end user will see
|
||||
software being installed. They will not be able to continue until
|
||||
software is installed.
|
||||
During the <b>Remote Management</b> screen, the end user will see
|
||||
selected software being installed. They won't be able to continue
|
||||
until software is installed.
|
||||
</p>
|
||||
<p>
|
||||
If there are any errors, they will be able to continue and will be
|
||||
instructed to contact their IT admin.
|
||||
</p>
|
||||
<img
|
||||
className={`${baseClass}__preview-img`}
|
||||
src={InstallSoftwarePreviewImg}
|
||||
alt="End user experience during the macOS setup assistant with selected
|
||||
software being installed"
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video
|
||||
className={`${baseClass}__preview-video`}
|
||||
src={InstallSoftwareEndUserPreview}
|
||||
controls
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@
|
|||
font-weight: normal;
|
||||
}
|
||||
|
||||
&__preview-img {
|
||||
&__preview-video {
|
||||
margin-top: $pad-xxlarge;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: $border-radius-large;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ import {
|
|||
errorNoSetupExperienceScript,
|
||||
} from "test/handlers/setup-experience-handlers";
|
||||
|
||||
import SetupExperienceScript from "./SetupExperienceScript";
|
||||
import RunScript from "./RunScript";
|
||||
|
||||
describe("SetupExperienceScript", () => {
|
||||
describe("RunScript", () => {
|
||||
it("should render the script uploader when no script has been uploaded", async () => {
|
||||
mockServer.use(errorNoSetupExperienceScript);
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
|
||||
render(<SetupExperienceScript currentTeamId={1} />);
|
||||
render(<RunScript currentTeamId={1} />);
|
||||
|
||||
expect(await screen.findByRole("button", { name: "Upload" })).toBeVisible();
|
||||
});
|
||||
|
|
@ -24,7 +24,7 @@ describe("SetupExperienceScript", () => {
|
|||
mockServer.use(defaultSetupExperienceScriptHandler);
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
|
||||
render(<SetupExperienceScript currentTeamId={1} />);
|
||||
render(<RunScript currentTeamId={1} />);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Script will run during setup:")
|
||||
|
|
@ -9,27 +9,30 @@ import {
|
|||
import mdmAPI, {
|
||||
IGetSetupExperienceScriptResponse,
|
||||
} from "services/entities/mdm";
|
||||
import configAPI from "services/entities/config";
|
||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { API_NO_TEAM_ID, ITeamConfig } from "interfaces/team";
|
||||
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
import DataError from "components/DataError";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import SetupExperiencePreview from "./components/SetupExperienceScriptPreview";
|
||||
import SetupExperienceScriptUploader from "./components/SetupExperienceScriptUploader";
|
||||
import SetupExperienceScriptCard from "./components/SetupExperienceScriptCard";
|
||||
import DeleteSetupExperienceScriptModal from "./components/DeleteSetupExperienceScriptModal";
|
||||
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
|
||||
import { getManualAgentInstallSetting } from "../BootstrapPackage/BootstrapPackage";
|
||||
|
||||
const baseClass = "setup-experience-script";
|
||||
const baseClass = "run-script";
|
||||
|
||||
interface ISetupExperienceScriptProps {
|
||||
interface IRunScriptProps {
|
||||
currentTeamId: number;
|
||||
}
|
||||
|
||||
const SetupExperienceScript = ({
|
||||
currentTeamId,
|
||||
}: ISetupExperienceScriptProps) => {
|
||||
const RunScript = ({ currentTeamId }: IRunScriptProps) => {
|
||||
const [showDeleteScriptModal, setShowDeleteScriptModal] = useState(false);
|
||||
|
||||
const {
|
||||
|
|
@ -45,6 +48,24 @@ const SetupExperienceScript = ({
|
|||
{ ...DEFAULT_USE_QUERY_OPTIONS, retry: false }
|
||||
);
|
||||
|
||||
const { data: globalConfig, isLoading: isLoadingGlobalConfig } = useQuery<
|
||||
IConfig,
|
||||
Error
|
||||
>(["config", currentTeamId], () => configAPI.loadAll(), {
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
enabled: currentTeamId === API_NO_TEAM_ID,
|
||||
});
|
||||
|
||||
const { data: teamConfig, isLoading: isLoadingTeamConfig } = useQuery<
|
||||
ILoadTeamResponse,
|
||||
Error,
|
||||
ITeamConfig
|
||||
>(["team", currentTeamId], () => teamsAPI.load(currentTeamId), {
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
enabled: currentTeamId !== API_NO_TEAM_ID,
|
||||
select: (res) => res.team,
|
||||
});
|
||||
|
||||
const onUpload = () => {
|
||||
refetchScript();
|
||||
};
|
||||
|
|
@ -56,9 +77,14 @@ const SetupExperienceScript = ({
|
|||
};
|
||||
|
||||
const scriptUploaded = true;
|
||||
const hasManualAgentInstall = getManualAgentInstallSetting(
|
||||
currentTeamId,
|
||||
globalConfig,
|
||||
teamConfig
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
if (isLoading) {
|
||||
if (isLoading || isLoadingGlobalConfig || isLoadingTeamConfig) {
|
||||
<Spinner />;
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +93,7 @@ const SetupExperienceScript = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__content`}>
|
||||
<SetupExperienceContentContainer>
|
||||
<div className={`${baseClass}__description-container`}>
|
||||
<p className={`${baseClass}__description`}>
|
||||
Upload a script to run on hosts that automatically enroll to Fleet.
|
||||
|
|
@ -81,6 +107,7 @@ const SetupExperienceScript = ({
|
|||
{!scriptUploaded || !script ? (
|
||||
<SetupExperienceScriptUploader
|
||||
currentTeamId={currentTeamId}
|
||||
hasManualAgentInstall={hasManualAgentInstall}
|
||||
onUpload={onUpload}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -96,12 +123,12 @@ const SetupExperienceScript = ({
|
|||
)}
|
||||
</div>
|
||||
<SetupExperiencePreview />
|
||||
</div>
|
||||
</SetupExperienceContentContainer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<section className={baseClass}>
|
||||
<SectionHeader title="Run script" />
|
||||
<>{renderContent()}</>
|
||||
{showDeleteScriptModal && script && (
|
||||
|
|
@ -112,8 +139,8 @@ const SetupExperienceScript = ({
|
|||
onExit={() => setShowDeleteScriptModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupExperienceScript;
|
||||
export default RunScript;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.run-script {
|
||||
&__description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__learn-how-link {
|
||||
margin-bottom: $pad-large;
|
||||
}
|
||||
|
||||
&__run-message {
|
||||
margin: 0 0 $pad-small;
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
|
||||
import Card from "components/Card";
|
||||
|
||||
import InstallSoftwarePreviewImg from "../../../../../../../../assets/images/install-software-preview.png";
|
||||
import SetupExperienceRunScriptPreview from "../../../../../../../../assets/images/run-script-preview.png";
|
||||
|
||||
const baseClass = "setup-experience-script-preview";
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ const SetupExperienceScriptPreview = () => {
|
|||
</p>
|
||||
<img
|
||||
className={`${baseClass}__preview-img`}
|
||||
src={InstallSoftwarePreviewImg}
|
||||
src={SetupExperienceRunScriptPreview}
|
||||
alt="End user experience during the macOS setup assistant with the uploaded
|
||||
script being run"
|
||||
/>
|
||||
|
|
@ -9,5 +9,6 @@
|
|||
margin-top: $pad-xxlarge;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: $border-radius-large;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,12 +11,14 @@ const baseClass = "setup-experience-script-uploader";
|
|||
|
||||
interface ISetupExperienceScriptUploaderProps {
|
||||
currentTeamId: number;
|
||||
hasManualAgentInstall: boolean;
|
||||
onUpload: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SetupExperienceScriptUploader = ({
|
||||
currentTeamId,
|
||||
hasManualAgentInstall,
|
||||
onUpload,
|
||||
className,
|
||||
}: ISetupExperienceScriptUploaderProps) => {
|
||||
|
|
@ -47,6 +49,14 @@ const SetupExperienceScriptUploader = ({
|
|||
setShowLoading(false);
|
||||
};
|
||||
|
||||
const manuallyInstallTooltipText = (
|
||||
<>
|
||||
Disabled because you manually install Fleet's agent (
|
||||
<b>Bootstrap package {">"} Advanced options</b>). Use your bootstrap
|
||||
package to install software during the setup experience.
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<FileUploader
|
||||
className={classNames}
|
||||
|
|
@ -56,6 +66,8 @@ const SetupExperienceScriptUploader = ({
|
|||
buttonMessage="Upload"
|
||||
onFileUpload={onUploadFile}
|
||||
isLoading={showLoading}
|
||||
disabled={hasManualAgentInstall}
|
||||
buttonTooltip={hasManualAgentInstall && manuallyInstallTooltipText}
|
||||
gitopsCompatible
|
||||
/>
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./RunScript";
|
||||
|
|
@ -20,6 +20,7 @@ import SetupAssistantProfileUploader from "./components/SetupAssistantProfileUpl
|
|||
import SetupAssistantProfileCard from "./components/SetupAssistantProfileCard/SetupAssistantProfileCard";
|
||||
import DeleteAutoEnrollmentProfile from "./components/DeleteAutoEnrollmentProfile";
|
||||
import AdvancedOptionsForm from "./components/AdvancedOptionsForm";
|
||||
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
|
||||
|
||||
const baseClass = "setup-assistant";
|
||||
|
||||
|
|
@ -90,12 +91,12 @@ const SetupAssistant = ({ currentTeamId }: ISetupAssistantProps) => {
|
|||
const enrollmentProfileNotFound = enrollmentProfileError?.status === 404;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<section className={baseClass}>
|
||||
<SectionHeader title="Setup assistant" />
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={`${baseClass}__content`}>
|
||||
<SetupExperienceContentContainer>
|
||||
<div className={`${baseClass}__upload-container`}>
|
||||
<p className={`${baseClass}__section-description`}>
|
||||
Add an automatic enrollment profile to customize the macOS Setup
|
||||
|
|
@ -126,7 +127,7 @@ const SetupAssistant = ({ currentTeamId }: ISetupAssistantProps) => {
|
|||
<div className={`${baseClass}__preview-container`}>
|
||||
<SetupAssistantPreview />
|
||||
</div>
|
||||
</div>
|
||||
</SetupExperienceContentContainer>
|
||||
)}
|
||||
{showDeleteProfileModal && (
|
||||
<DeleteAutoEnrollmentProfile
|
||||
|
|
@ -135,7 +136,7 @@ const SetupAssistant = ({ currentTeamId }: ISetupAssistantProps) => {
|
|||
onCancel={() => setShowDeleteProfileModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
.setup-assistant {
|
||||
&__content {
|
||||
max-width: $break-xxl;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: $pad-xxlarge;
|
||||
}
|
||||
|
||||
&__upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -16,10 +8,4 @@
|
|||
&__section-description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
&__content {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
|
||||
import Card from "components/Card";
|
||||
|
||||
import OsPrefillPreview from "../../../../../../../../assets/images/os-prefill-preview.gif";
|
||||
import SetupAssistantEndUserPreview from "../../../../../../../../assets/videos/setup-assistant-preview.mp4";
|
||||
|
||||
const baseClass = "setup-assistant-preview";
|
||||
|
||||
|
|
@ -18,11 +18,14 @@ const SetupAssistantPreview = () => {
|
|||
By adding an automatic enrollment profile you can customize which
|
||||
screens are displayed and more.
|
||||
</p>
|
||||
<img
|
||||
className={`${baseClass}__preview-img`}
|
||||
src={OsPrefillPreview}
|
||||
alt="End user experience during the macOS setup assistant customised by
|
||||
an automatic enrollment profile"
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video
|
||||
className={`${baseClass}__preview-video`}
|
||||
src={SetupAssistantEndUserPreview}
|
||||
controls
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
font-weight: normal;
|
||||
}
|
||||
|
||||
&__preview-img {
|
||||
&__preview-video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: $pad-xxlarge auto 0;
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
.setup-experience-script {
|
||||
&__content {
|
||||
max-width: $break-xxl;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: $pad-xxlarge;
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__learn-how-link {
|
||||
margin-bottom: $pad-large;
|
||||
}
|
||||
|
||||
&__run-message {
|
||||
margin: 0 0 $pad-small;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
&__content {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./SetupExperienceScript";
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
const baseClass = "setup-experience-content-container";
|
||||
|
||||
interface ISetupExperienceContentContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SetupExperienceContentContainer = ({
|
||||
children,
|
||||
className,
|
||||
}: ISetupExperienceContentContainerProps) => {
|
||||
const classNames = classnames(baseClass, className);
|
||||
return <div className={classNames}>{children}</div>;
|
||||
};
|
||||
|
||||
export default SetupExperienceContentContainer;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.setup-experience-content-container {
|
||||
max-width: $break-xxl;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(150px, 450px) minmax(50%, 1fr);
|
||||
gap: $pad-xxlarge;
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./SetupExperienceContentContainer";
|
||||
|
|
@ -48,7 +48,9 @@ export const isDDMProfile = (profile: IMdmProfile | IHostMdmProfile) => {
|
|||
|
||||
interface IUpdateSetupExperienceBody {
|
||||
team_id?: number;
|
||||
enable_release_device_manually: boolean;
|
||||
enable_end_user_authentication?: boolean;
|
||||
enable_release_device_manually?: boolean;
|
||||
manual_agent_install?: boolean;
|
||||
}
|
||||
|
||||
export interface IAppleSetupEnrollmentProfileResponse {
|
||||
|
|
@ -248,6 +250,19 @@ const mdmService = {
|
|||
});
|
||||
},
|
||||
|
||||
updateSetupExperienceSettings: (updateData: IUpdateSetupExperienceBody) => {
|
||||
const { MDM_SETUP_EXPERIENCE } = endpoints;
|
||||
const body = {
|
||||
...updateData,
|
||||
};
|
||||
|
||||
if (updateData.team_id === API_NO_TEAM_ID) {
|
||||
delete body.team_id;
|
||||
}
|
||||
|
||||
return sendRequest("PATCH", MDM_SETUP_EXPERIENCE, body);
|
||||
},
|
||||
|
||||
updateReleaseDeviceSetting: (teamId: number, isEnabled: boolean) => {
|
||||
const { MDM_SETUP_EXPERIENCE } = endpoints;
|
||||
|
||||
|
|
|
|||
5
frontend/typings/index.d.ts
vendored
5
frontend/typings/index.d.ts
vendored
|
|
@ -22,6 +22,11 @@ declare module "*.pdf" {
|
|||
export = value;
|
||||
}
|
||||
|
||||
declare module "*.mp4" {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare const featureFlags: {
|
||||
[key: string]: type;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue