fleet/frontend/pages/hosts/components/ScriptDetailsModal/ScriptDetailsModal.tsx
RachelElysia c9e66b221e
Frontend: Lint warning cleanup part 1 (#43411)
## Issue
- First batch of @iansltx 's work of cleaning up lint warnings #43387 

## Description
- Quick PR review and grabbed as many confirmed low-risk quick wins as I
could `git checkout lint-cleanup <file/path/1> <file/path/2>`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

This release contains internal code improvements with one minor UI
tweak:

* **Style**
* Dropdown menu background color adjusted for clearer contrast in action
lists
* **Refactor**
* Improved type safety across the codebase with stricter TypeScript
annotations
  * Removed unused imports and constants to reduce code clutter
* Enhanced React hook dependency arrays for more consistent component
behavior
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Rachel Perkins <rachel@Rachels-MacBook-Pro.local>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2026-04-10 19:49:52 -05:00

299 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, {
useCallback,
useContext,
useRef,
useState,
useEffect,
} from "react";
import { format } from "date-fns";
import { useQuery } from "react-query";
import FileSaver from "file-saver";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import scriptAPI from "services/entities/scripts";
import { IHostScript } from "interfaces/script";
import Modal from "components/Modal";
import ModalFooter from "components/ModalFooter";
import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
import Icon from "components/Icon";
import Textarea from "components/Textarea";
import DataError from "components/DataError";
import ActionsDropdown from "components/ActionsDropdown";
import { generateActionDropdownOptions } from "pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import { IPaginatedListScript } from "pages/hosts/ManageHostsPage/components/RunScriptBatchPaginatedList/RunScriptBatchPaginatedList";
import RunScriptHelpText from "./RunScriptHelpText";
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;
/** optional onClose to allow both "go back" behavior and "close" behavior depending on context */
onClose?: () => void;
onDelete?: () => void;
runScriptHelpText?: boolean;
showHostScriptActions?: boolean;
onClickRun?: (script: IHostScript) => void;
hostTeamId?: number | null;
selectedScriptId?: number;
selectedScriptDetails?: PartialOrFullHostScript | IPaginatedListScript | null;
selectedScriptContent?: string;
isLoadingScriptContent?: boolean;
isScriptContentError?: Error | null;
isHidden?: boolean;
onClickRunDetails?: (scriptExecutionId: string) => void;
teamIdForApi?: number;
suppressSecondaryActions?: boolean;
customPrimaryButtons?: React.ReactNode;
}
const ScriptDetailsModal = ({
onCancel,
onClose,
onDelete,
onClickRun,
runScriptHelpText = false,
showHostScriptActions = false,
hostTeamId,
selectedScriptId,
selectedScriptDetails,
selectedScriptContent,
isLoadingScriptContent,
isScriptContentError,
isHidden = false,
onClickRunDetails,
teamIdForApi,
suppressSecondaryActions = false,
customPrimaryButtons,
}: IScriptDetailsModalProps) => {
// 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);
}
};
const {
currentUser,
isGlobalAdmin,
isAnyTeamAdmin,
isGlobalMaintainer,
isAnyTeamMaintainer,
isTeamTechnician,
isGlobalTechnician,
} = useContext(AppContext);
const isTechnician = !!isTeamTechnician || !!isGlobalTechnician;
const canRunScripts = !!(
isGlobalAdmin ||
isAnyTeamAdmin ||
isGlobalMaintainer ||
isAnyTeamMaintainer
);
const { renderFlash } = useContext(NotificationContext);
// handle multiple possibilities for `selectedScriptDetails`
let scriptId: number | null = null;
if (selectedScriptId) {
scriptId = selectedScriptId;
} else if (selectedScriptDetails) {
if ("script_id" in selectedScriptDetails) {
scriptId = selectedScriptDetails.script_id;
} else if ("id" in selectedScriptDetails) {
scriptId = selectedScriptDetails.id;
}
}
const {
data: scriptContent,
error: isSelectedScriptContentError,
isLoading: isLoadingSelectedScriptContent,
} = useQuery<string, Error>(
["scriptContent", scriptId],
() =>
scriptId
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
scriptAPI.downloadScript(scriptId)
: Promise.resolve(null),
{
refetchOnWindowFocus: false,
enabled: !selectedScriptContent && !!scriptId,
}
);
// For scrollable modal
useEffect(() => {
checkScroll();
window.addEventListener("resize", checkScroll);
return () => window.removeEventListener("resize", checkScroll);
}, [scriptContent]); // Re-run when data changes
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", "Couldnt 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) => {
switch (action) {
case "showRunDetails": {
if (script.last_execution?.execution_id) {
onClickRunDetails &&
onClickRunDetails(script.last_execution?.execution_id);
}
break;
}
case "run": {
// should always be present if these actions are visible
onClickRun && onClickRun(script);
break;
}
default: // do nothing
}
},
[onClickRunDetails, onClickRun]
);
const shouldShowFooter =
!isLoadingScriptContent && selectedScriptDetails !== undefined;
const renderFooter = () => {
return (
<ModalFooter
isTopScrolling={isTopScrolling}
secondaryButtons={
suppressSecondaryActions ? undefined : (
<>
<Button
className={`${baseClass}__action-button`}
variant="icon"
onClick={() => onClickDownload()}
>
<Icon name="download" />
</Button>
<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>
)}
/>
</>
)
}
primaryButtons={
customPrimaryButtons || (
<>
{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,
selectedScriptDetails as IHostScript
)}
menuPlacement="top"
/>
</div>
)}
<Button onClick={onCancel}>Close</Button>
</>
)
}
/>
);
};
const renderContent = () => {
if (isLoadingScriptContent || isLoadingSelectedScriptContent) {
return <Spinner />;
}
if (isScriptContentError || isSelectedScriptContentError) {
return <DataError description="Close this modal and try again." />;
}
return (
<div
className={`${baseClass}__script-content modal-scrollable-content`}
ref={topDivRef}
>
<Textarea label="Script content:" variant="code">
{scriptContent}
</Textarea>
{runScriptHelpText && (
<RunScriptHelpText
className="form-field__help-text"
isTechnician={isTechnician}
canRunScripts={canRunScripts}
teamId={teamIdForApi}
/>
)}
</div>
);
};
return (
<Modal
className={baseClass}
title={selectedScriptDetails?.name || "Script details"}
width="large"
onExit={onClose ?? onCancel}
isHidden={isHidden}
>
{renderContent()}
{shouldShowFooter && renderFooter()}
</Modal>
);
};
export default ScriptDetailsModal;