2024-12-03 14:09:53 +00:00
|
|
|
|
import React, {
|
|
|
|
|
|
useCallback,
|
|
|
|
|
|
useContext,
|
|
|
|
|
|
useRef,
|
|
|
|
|
|
useState,
|
|
|
|
|
|
useEffect,
|
|
|
|
|
|
} from "react";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
import { format } from "date-fns";
|
2024-11-08 14:22:49 +00:00
|
|
|
|
import {
|
|
|
|
|
|
useQuery,
|
|
|
|
|
|
RefetchOptions,
|
|
|
|
|
|
RefetchQueryFilters,
|
|
|
|
|
|
QueryObserverResult,
|
|
|
|
|
|
} from "react-query";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
import FileSaver from "file-saver";
|
|
|
|
|
|
|
|
|
|
|
|
import { AppContext } from "context/app";
|
|
|
|
|
|
import { NotificationContext } from "context/notification";
|
2024-11-08 14:22:49 +00:00
|
|
|
|
import scriptAPI, { IHostScriptsResponse } from "services/entities/scripts";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
import { IHostScript } from "interfaces/script";
|
2024-11-08 14:22:49 +00:00
|
|
|
|
import { IApiError, getErrorReason } from "interfaces/errors";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
|
|
|
|
|
|
import Modal from "components/Modal";
|
2024-12-03 14:09:53 +00:00
|
|
|
|
import ModalFooter from "components/ModalFooter";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
import Button from "components/buttons/Button";
|
|
|
|
|
|
import Spinner from "components/Spinner";
|
|
|
|
|
|
import Icon from "components/Icon";
|
|
|
|
|
|
import Textarea from "components/Textarea";
|
|
|
|
|
|
import CustomLink from "components/CustomLink";
|
|
|
|
|
|
import DataError from "components/DataError";
|
|
|
|
|
|
import paths from "router/paths";
|
|
|
|
|
|
import ActionsDropdown from "components/ActionsDropdown";
|
|
|
|
|
|
import { generateActionDropdownOptions } from "pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig";
|
UI - GitOps Mode: Core abstractions, first batch of applications (#26401)
## For #26229 – Part 1

- This PR contains the core abstractions, routes, API updates, and types
for GitOps mode in the UI. Since this work will touch essentially every
part of the Fleet UI, it is ripe for merge conflicts. To mitigate such
conflicts, I'll be merging this work in a number of iterative PRs. ~To
effectively gate any of this work from showing until it is all merged to
`main`, [this commit](feedbb2d4c25ec2e304e1f18d409cee62f6752ed) hides
the settings section that allows enabling/disabling this setting,
effectively feature flagging the entire thing. In the last of these
iterative PRs, that commit will be reverted to engage the entire
feature. For testing purposes, reviewers can `git revert
feedbb2d4c25ec2e304e1f18d409cee62f6752ed` locally~ The new settings
section for this feature is feature flagged until all PRs are merged -
to show the setting section while testing, run `ALLOW_GITOPS_MODE=true
NODE_ENV=development yarn run webpack --progress --watch` in place of
`make generate-dev`
- Changes file will be added and feature flag removed in the last PR
- [x] Settings page with routing, form, API integration (hidden until
last PR)
- [x] Activities
- [x] Navbar indicator
- Apply GOM conditional UI to:
- [x] Manage enroll secret modal: .5
- Controls >
- [x] Scripts:
- Setup experience >
- [x] Install software > Select software modal
- [x] OS Settings >
- [x] Custom settings
- [x] Disk encryption
- [x] OS Updates
2/18/25, added to this PR:
- [x] Controls > Setup experience > Run script
- [x] Software >
- [x] Manage automations modal
- [x] Add software >
- [x] App Store (VPP)
- [x] Custom package
- [x] Queries
- [x] Manage
- [x] Automations modal
- [x] New
- [x] Edit
- [x] Policies
- [x] Manage
- [x] New
- [x] Edit
- Manage automations
- [x] Calendar events
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-02-20 16:41:07 +00:00
|
|
|
|
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
|
|
|
|
|
|
const baseClass = "script-details-modal";
|
|
|
|
|
|
|
|
|
|
|
|
type PartialOrFullHostScript =
|
|
|
|
|
|
| Pick<IHostScript, "script_id" | "name"> // Use on Scripts page does not include last_execution
|
|
|
|
|
|
| IHostScript;
|
|
|
|
|
|
|
|
|
|
|
|
interface IScriptDetailsModalProps {
|
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
|
onDelete: () => void;
|
2024-11-08 14:22:49 +00:00
|
|
|
|
/** Help text on manage scripts page's modal but not on host detail's page modal */
|
2024-11-06 17:48:11 +00:00
|
|
|
|
runScriptHelpText?: boolean;
|
2024-11-08 14:22:49 +00:00
|
|
|
|
/** Host actions dropdown on host details page's modal but not on manage scripts page's modal */
|
2024-11-06 17:48:11 +00:00
|
|
|
|
showHostScriptActions?: boolean;
|
|
|
|
|
|
setRunScriptRequested?: (value: boolean) => void;
|
|
|
|
|
|
hostId?: number | null;
|
|
|
|
|
|
hostTeamId?: number | null;
|
2024-11-08 14:22:49 +00:00
|
|
|
|
refetchHostScripts?: <TPageData>(
|
|
|
|
|
|
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
|
|
|
|
|
|
) => Promise<QueryObserverResult<IHostScriptsResponse, IApiError>>;
|
2024-11-06 17:48:11 +00:00
|
|
|
|
selectedScriptDetails?: PartialOrFullHostScript;
|
|
|
|
|
|
selectedScriptContent?: string;
|
|
|
|
|
|
isLoadingScriptContent?: boolean;
|
|
|
|
|
|
isScriptContentError?: Error | null;
|
|
|
|
|
|
isHidden?: boolean;
|
|
|
|
|
|
onClickRunDetails?: (scriptExecutionId: string) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ScriptDetailsModal = ({
|
|
|
|
|
|
onCancel,
|
|
|
|
|
|
onDelete,
|
|
|
|
|
|
runScriptHelpText = false,
|
|
|
|
|
|
showHostScriptActions = false,
|
|
|
|
|
|
setRunScriptRequested,
|
|
|
|
|
|
hostId,
|
|
|
|
|
|
hostTeamId,
|
|
|
|
|
|
refetchHostScripts,
|
|
|
|
|
|
selectedScriptDetails,
|
|
|
|
|
|
selectedScriptContent,
|
|
|
|
|
|
isLoadingScriptContent,
|
|
|
|
|
|
isScriptContentError,
|
|
|
|
|
|
isHidden = false,
|
|
|
|
|
|
onClickRunDetails,
|
|
|
|
|
|
}: IScriptDetailsModalProps) => {
|
2024-12-03 14:09:53 +00:00
|
|
|
|
// For scrollable modal
|
|
|
|
|
|
const [isTopScrolling, setIsTopScrolling] = useState(false);
|
|
|
|
|
|
const topDivRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
const checkScroll = () => {
|
|
|
|
|
|
if (topDivRef.current) {
|
|
|
|
|
|
const isScrolling =
|
|
|
|
|
|
topDivRef.current.scrollHeight > topDivRef.current.clientHeight;
|
|
|
|
|
|
setIsTopScrolling(isScrolling);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-11-06 17:48:11 +00:00
|
|
|
|
const { currentUser } = useContext(AppContext);
|
|
|
|
|
|
const { renderFlash } = useContext(NotificationContext);
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
data: scriptContent,
|
|
|
|
|
|
error: isSelectedScriptContentError,
|
|
|
|
|
|
isLoading: isLoadingSelectedScriptContent,
|
|
|
|
|
|
} = useQuery<any, Error>(
|
|
|
|
|
|
["scriptContent", selectedScriptDetails?.script_id],
|
|
|
|
|
|
() =>
|
2024-11-08 14:22:49 +00:00
|
|
|
|
selectedScriptDetails?.script_id
|
|
|
|
|
|
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
|
|
scriptAPI.downloadScript(selectedScriptDetails.script_id!)
|
2024-11-06 17:48:11 +00:00
|
|
|
|
: Promise.resolve(null),
|
|
|
|
|
|
{
|
|
|
|
|
|
refetchOnWindowFocus: false,
|
|
|
|
|
|
enabled: !selectedScriptContent && !!selectedScriptDetails?.script_id,
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
2024-12-03 14:09:53 +00:00
|
|
|
|
|
|
|
|
|
|
// For scrollable modal
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
checkScroll();
|
|
|
|
|
|
window.addEventListener("resize", checkScroll);
|
|
|
|
|
|
return () => window.removeEventListener("resize", checkScroll);
|
|
|
|
|
|
}, [scriptContent]); // Re-run when data changes
|
|
|
|
|
|
|
2024-11-06 17:48:11 +00:00
|
|
|
|
const getScriptContent = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const content = selectedScriptContent || scriptContent;
|
|
|
|
|
|
const formatDate = format(new Date(), "yyyy-MM-dd");
|
|
|
|
|
|
const filename = `${formatDate} ${
|
|
|
|
|
|
selectedScriptDetails?.name || "Script details"
|
|
|
|
|
|
}`;
|
|
|
|
|
|
const file = new File([content], filename);
|
|
|
|
|
|
FileSaver.saveAs(file);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
renderFlash("error", "Couldn’t Download. Please try again.");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onClickDownload = () => {
|
|
|
|
|
|
if (selectedScriptContent) {
|
|
|
|
|
|
const formatDate = format(new Date(), "yyyy-MM-dd");
|
|
|
|
|
|
const filename = `${formatDate} ${selectedScriptDetails?.name}`;
|
|
|
|
|
|
const file = new File([selectedScriptContent], filename);
|
|
|
|
|
|
FileSaver.saveAs(file);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
getScriptContent();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onSelectMoreActions = useCallback(
|
|
|
|
|
|
async (action: string, script: IHostScript) => {
|
2024-11-08 14:22:49 +00:00
|
|
|
|
if (hostId && !!setRunScriptRequested && !!refetchHostScripts) {
|
2024-11-06 17:48:11 +00:00
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "showRunDetails": {
|
2024-11-08 14:22:49 +00:00
|
|
|
|
if (script.last_execution?.execution_id) {
|
|
|
|
|
|
onClickRunDetails &&
|
|
|
|
|
|
onClickRunDetails(script.last_execution?.execution_id);
|
|
|
|
|
|
}
|
2024-11-06 17:48:11 +00:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case "run": {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setRunScriptRequested && setRunScriptRequested(true);
|
|
|
|
|
|
await scriptAPI.runScript({
|
|
|
|
|
|
host_id: hostId,
|
|
|
|
|
|
script_id: script.script_id,
|
|
|
|
|
|
});
|
|
|
|
|
|
renderFlash(
|
|
|
|
|
|
"success",
|
|
|
|
|
|
"Script is running or will run when the host comes online."
|
|
|
|
|
|
);
|
|
|
|
|
|
refetchHostScripts();
|
2024-11-08 14:22:49 +00:00
|
|
|
|
|
|
|
|
|
|
onCancel(); // Running a script returns to run script modal
|
2024-11-06 17:48:11 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
renderFlash("error", getErrorReason(e));
|
|
|
|
|
|
setRunScriptRequested(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
default: // do nothing
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[
|
|
|
|
|
|
hostId,
|
|
|
|
|
|
onClickRunDetails,
|
|
|
|
|
|
setRunScriptRequested,
|
|
|
|
|
|
refetchHostScripts,
|
|
|
|
|
|
renderFlash,
|
2024-11-08 14:22:49 +00:00
|
|
|
|
onCancel,
|
2024-11-06 17:48:11 +00:00
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2024-12-03 14:09:53 +00:00
|
|
|
|
const shouldShowFooter =
|
|
|
|
|
|
!isLoadingScriptContent && selectedScriptDetails !== undefined;
|
2024-11-06 17:48:11 +00:00
|
|
|
|
|
|
|
|
|
|
const renderFooter = () => {
|
|
|
|
|
|
if (!shouldShowFooter) {
|
2024-12-03 14:09:53 +00:00
|
|
|
|
return null;
|
2024-11-06 17:48:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2024-12-03 14:09:53 +00:00
|
|
|
|
<ModalFooter
|
|
|
|
|
|
isTopScrolling={isTopScrolling}
|
|
|
|
|
|
secondaryButtons={
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
className={`${baseClass}__action-button`}
|
|
|
|
|
|
variant="icon"
|
|
|
|
|
|
onClick={() => onClickDownload()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Icon name="download" />
|
|
|
|
|
|
</Button>
|
UI - GitOps Mode: Core abstractions, first batch of applications (#26401)
## For #26229 – Part 1

- This PR contains the core abstractions, routes, API updates, and types
for GitOps mode in the UI. Since this work will touch essentially every
part of the Fleet UI, it is ripe for merge conflicts. To mitigate such
conflicts, I'll be merging this work in a number of iterative PRs. ~To
effectively gate any of this work from showing until it is all merged to
`main`, [this commit](feedbb2d4c25ec2e304e1f18d409cee62f6752ed) hides
the settings section that allows enabling/disabling this setting,
effectively feature flagging the entire thing. In the last of these
iterative PRs, that commit will be reverted to engage the entire
feature. For testing purposes, reviewers can `git revert
feedbb2d4c25ec2e304e1f18d409cee62f6752ed` locally~ The new settings
section for this feature is feature flagged until all PRs are merged -
to show the setting section while testing, run `ALLOW_GITOPS_MODE=true
NODE_ENV=development yarn run webpack --progress --watch` in place of
`make generate-dev`
- Changes file will be added and feature flag removed in the last PR
- [x] Settings page with routing, form, API integration (hidden until
last PR)
- [x] Activities
- [x] Navbar indicator
- Apply GOM conditional UI to:
- [x] Manage enroll secret modal: .5
- Controls >
- [x] Scripts:
- Setup experience >
- [x] Install software > Select software modal
- [x] OS Settings >
- [x] Custom settings
- [x] Disk encryption
- [x] OS Updates
2/18/25, added to this PR:
- [x] Controls > Setup experience > Run script
- [x] Software >
- [x] Manage automations modal
- [x] Add software >
- [x] App Store (VPP)
- [x] Custom package
- [x] Queries
- [x] Manage
- [x] Automations modal
- [x] New
- [x] Edit
- [x] Policies
- [x] Manage
- [x] New
- [x] Edit
- Manage automations
- [x] Calendar events
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-02-20 16:41:07 +00:00
|
|
|
|
<GitOpsModeTooltipWrapper
|
|
|
|
|
|
position="bottom"
|
|
|
|
|
|
renderChildren={(disableChildren) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
disabled={disableChildren}
|
|
|
|
|
|
className={`${baseClass}__action-button`}
|
|
|
|
|
|
variant="icon"
|
|
|
|
|
|
onClick={onDelete}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Icon name="trash" color="ui-fleet-black-75" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
2024-12-03 14:09:53 +00:00
|
|
|
|
</>
|
|
|
|
|
|
}
|
|
|
|
|
|
primaryButtons={
|
|
|
|
|
|
<>
|
|
|
|
|
|
{showHostScriptActions && selectedScriptDetails && (
|
|
|
|
|
|
<div className={`${baseClass}__manage-automations-wrapper`}>
|
|
|
|
|
|
<ActionsDropdown
|
|
|
|
|
|
className={`${baseClass}__manage-automations-dropdown`}
|
|
|
|
|
|
onChange={(value) =>
|
|
|
|
|
|
onSelectMoreActions(
|
|
|
|
|
|
value,
|
|
|
|
|
|
selectedScriptDetails as IHostScript
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="More actions"
|
|
|
|
|
|
isSearchable={false}
|
|
|
|
|
|
options={generateActionDropdownOptions(
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
hostTeamId || null,
|
2024-11-06 17:48:11 +00:00
|
|
|
|
selectedScriptDetails as IHostScript
|
2024-12-03 14:09:53 +00:00
|
|
|
|
)}
|
|
|
|
|
|
menuPlacement="top"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Button onClick={onCancel} variant="brand">
|
|
|
|
|
|
Done
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
2024-11-06 17:48:11 +00:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderContent = () => {
|
|
|
|
|
|
if (isLoadingScriptContent || isLoadingSelectedScriptContent) {
|
|
|
|
|
|
return <Spinner />;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isScriptContentError || isSelectedScriptContentError) {
|
|
|
|
|
|
return <DataError description="Close this modal and try again." />;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2024-12-03 14:09:53 +00:00
|
|
|
|
<div
|
|
|
|
|
|
className={`${baseClass}__script-content modal-scrollable-content`}
|
|
|
|
|
|
ref={topDivRef}
|
|
|
|
|
|
>
|
2024-11-06 17:48:11 +00:00
|
|
|
|
<span>Script content:</span>
|
|
|
|
|
|
<Textarea className={`${baseClass}__script-content-textarea`}>
|
|
|
|
|
|
{scriptContent}
|
|
|
|
|
|
</Textarea>
|
|
|
|
|
|
{runScriptHelpText && (
|
|
|
|
|
|
<div className="form-field__help-text">
|
|
|
|
|
|
To run this script on a host, go to the{" "}
|
|
|
|
|
|
<CustomLink text="Hosts" url={paths.MANAGE_HOSTS} /> page and select
|
|
|
|
|
|
a host.
|
|
|
|
|
|
<br />
|
|
|
|
|
|
To run the script across multiple hosts, add a policy automation on
|
|
|
|
|
|
the <CustomLink text="Policies" url={paths.MANAGE_POLICIES} /> page.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
className={baseClass}
|
|
|
|
|
|
title={selectedScriptDetails?.name || "Script details"}
|
|
|
|
|
|
width="large"
|
|
|
|
|
|
onExit={onCancel}
|
|
|
|
|
|
isHidden={isHidden}
|
|
|
|
|
|
>
|
2024-12-03 14:09:53 +00:00
|
|
|
|
<>
|
|
|
|
|
|
{renderContent()}
|
|
|
|
|
|
{shouldShowFooter ? renderFooter() : undefined}
|
|
|
|
|
|
</>
|
2024-11-06 17:48:11 +00:00
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default ScriptDetailsModal;
|