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";
|
2025-10-01 17:15:30 +00:00
|
|
|
|
import { useQuery } 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";
|
2025-10-01 17:15:30 +00:00
|
|
|
|
import scriptAPI from "services/entities/scripts";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
import { IHostScript } from "interfaces/script";
|
|
|
|
|
|
|
|
|
|
|
|
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 DataError from "components/DataError";
|
|
|
|
|
|
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";
|
2025-04-28 23:32:41 +00:00
|
|
|
|
import { IPaginatedListScript } from "pages/hosts/ManageHostsPage/components/RunScriptBatchPaginatedList/RunScriptBatchPaginatedList";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
|
Technician role FE changes (#39494)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #38630
## Testing
- [x] QA'd all new/changed functionality manually
Screenshots below were taken with a **Team Technician** user. Same
changes apply for a **Global Technician**.
#### Controls > OS settings > Disk encryption
- Shows table without controls below.
- Shows empty state (doesn't allow to turn it on).
<img width="1915" height="886" alt="Screenshot 2026-02-10 at 12 24
25 PM"
src="https://github.com/user-attachments/assets/3f44d338-e728-4eb2-ad93-e30844201b52"
/>
<img width="1913" height="907" alt="Screenshot 2026-02-10 at 12 31
38 PM"
src="https://github.com/user-attachments/assets/71706e9e-0540-4c25-b5c0-3f7ccff3ba5a"
/>
#### Controls > OS settings > Custom settings
- Changed description to say **View configuration profiles that apply
custom settings.** instead of **Create and upload configuration profiles
to apply custom settings.**.
- **Add profile** not shown within table header.
- Trash can icon not shown when hovering over a row within the table.
- **Add profile** card not shown on empty state. Instead, "No
configuration profiles have been added." is shown.
<img width="1911" height="729" alt="Screenshot 2026-02-10 at 12 24
39 PM"
src="https://github.com/user-attachments/assets/aa68cbaf-4772-402d-9288-b4be2ddd3250"
/>
<img width="1912" height="650" alt="Screenshot 2026-02-10 at 12 28
48 PM"
src="https://github.com/user-attachments/assets/6a186172-b01f-4314-bb50-4cb533e13bce"
/>
#### Controls > Scripts > Library
- **Add script** not shown within table header.
- No actions shown when hovering over a table row.
- Can view script by clicking on a table row.
- Removed **To run the script across multiple hosts, add a policy
automation on the Policies page** line below **To run this script on a
host, go to the Hosts page and select a host.**.
- Updated copy to `To run this script on a host, go to the Hosts page
and select a host. Then, click Actions > Run script.`
<img width="1912" height="772" alt="Screenshot 2026-02-10 at 12 25
46 PM"
src="https://github.com/user-attachments/assets/83fbc1ec-3a6e-4bb5-865e-b5e7faef1e37"
/>
<img width="1732" height="761" alt="Screenshot 2026-02-11 at 3 50 33 PM"
src="https://github.com/user-attachments/assets/6dda97d7-fde2-4bcd-94b3-fa7368c65528"
/>
#### Labels
Can add label and filter by label
<img width="160" height="247" alt="Screenshot 2026-02-10 at 12 51 24 PM"
src="https://github.com/user-attachments/assets/ed63b708-27f8-4363-9d4f-9a7b0bf82b21"
/>
<img width="1901" height="856" alt="Screenshot 2026-02-10 at 12 35
07 PM"
src="https://github.com/user-attachments/assets/c2ef5e21-03ab-4955-a22f-cd6ca32f3179"
/>
<img width="1903" height="937" alt="Screenshot 2026-02-10 at 12 36
11 PM"
src="https://github.com/user-attachments/assets/d9d9f3bc-4d71-4c4b-902a-455eec9e057c"
/>
Can edit/delete labels created by themselves.
NOTE: my technician user ID is 37 - note that the **x** label belongs to
a different user id, while the second label belongs to ID 37, therefore
it can be edited and deleted.
<img width="1915" height="1152" alt="Screenshot 2026-02-10 at 12 38
29 PM"
src="https://github.com/user-attachments/assets/21f44c11-4e2d-456b-8547-90936b5d7602"
/>
<img width="1911" height="1154" alt="Screenshot 2026-02-10 at 12 38
42 PM"
src="https://github.com/user-attachments/assets/f9f7ea30-11b2-4d2d-9d71-de7299e4b451"
/>
Can delete manual label from host
https://github.com/user-attachments/assets/b64ba6dd-3f54-4dcd-9c57-7bede65122da
#### Host details
Can run scripts and view their results
<img width="1908" height="472" alt="Screenshot 2026-02-10 at 12 52
33 PM"
src="https://github.com/user-attachments/assets/d1e40339-ec52-47ff-bc53-c311498ffe80"
/>
<img width="1882" height="716" alt="Screenshot 2026-02-10 at 12 52
40 PM"
src="https://github.com/user-attachments/assets/dd0c2ec3-8cb8-4835-9c6d-f731a7434637"
/>
<img width="1915" height="718" alt="Screenshot 2026-02-10 at 12 52
48 PM"
src="https://github.com/user-attachments/assets/5e7a73e0-ac5b-4d38-b635-770f53dea9e3"
/>
<img width="1914" height="718" alt="Screenshot 2026-02-10 at 12 52
55 PM"
src="https://github.com/user-attachments/assets/b199c796-66b1-46bc-b2b5-fd35e8aa7a7c"
/>
Can run query associated to host as a live query
https://github.com/user-attachments/assets/7aea6f63-e443-4fa0-87dc-48bef84efa2f
#### Software
Doesn't show trash can icon on software installer card, just the
download one.
<img width="1423" height="838" alt="Screenshot 2026-02-10 at 1 33 53 PM"
src="https://github.com/user-attachments/assets/3a55c226-0bba-43ac-8594-7b5ac0a3684a"
/>
Can install/uninstall software on a host. Note that **Add software**
button is hidden (technicians can't add software).
<img width="1378" height="277" alt="Screenshot 2026-02-10 at 3 08 55 PM"
src="https://github.com/user-attachments/assets/bf413467-2071-48b6-b62b-f3a721b6057c"
/>
#### Queries
- Can run inherited queries on all hosts
https://github.com/user-attachments/assets/09f07e6b-a8c1-453e-81fd-4deb16005836
- Can run team queries on all hosts
https://github.com/user-attachments/assets/18b62dea-e159-40ea-b0ed-1d96b6bd40e7
- Can't manage automations or add queries (buttons are not shown at the
top-right corner)
#### Policies
Same as Queries
https://github.com/user-attachments/assets/2c24514a-2ae0-47a6-b631-6f9e48fc7b9c
#### Protected routes
Tested that I can't access routes that have restricted functionality for
this role, such as:
- **/controls/os-updates**, **/controls/setup-experience** and
**/controls/os-settings/certificates** => redirects to
**/controls/os-settings** ✅
- **/controls/scripts/progress** => redirects to
**/controls/scripts/library** ✅
- **/queries/new** and **/software/add/*** => renders access denied page
✅
---------
Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
2026-02-11 21:38:41 +00:00
|
|
|
|
import RunScriptHelpText from "./RunScriptHelpText";
|
|
|
|
|
|
|
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;
|
2025-10-01 17:15:30 +00:00
|
|
|
|
/** optional onClose to allow both "go back" behavior and "close" behavior depending on context */
|
|
|
|
|
|
onClose?: () => void;
|
2025-04-28 23:32:41 +00:00
|
|
|
|
onDelete?: () => void;
|
2024-11-06 17:48:11 +00:00
|
|
|
|
runScriptHelpText?: boolean;
|
|
|
|
|
|
showHostScriptActions?: boolean;
|
2025-10-01 17:15:30 +00:00
|
|
|
|
onClickRun?: (script: IHostScript) => void;
|
2024-11-06 17:48:11 +00:00
|
|
|
|
hostTeamId?: number | null;
|
UI: Batch script run detail page (#32333)
## For #31226
New features:
- Dynamic header for each possible state of a batch script run: Started,
Scheduled, and Finished (corresponds to tabs at
`/controls/scripts/progress`
- Unique tabs for each possible status of hosts targeted by a batch
script run: Ran, Errored, Pending, Incompatible, Canceled.
- Within each tab, sortable, paginated host results with output preview
and execution time.
- View script/run details, cancel a batch, view manage hosts page
filtered for the script batch run and a status.
- Global script batch runs activities and and Scripts progress rows now
navigate to this details page.
Cleanups and improvements:
- Expand tab count badge options using “alert”/“pending” variants across
hosts, policies, and query results.
- Misc cleanups and improvements

- [x] Changes file added for user-visible changes in `changes/`,
- [x] Updated automated tests - new tests tracked for follow-up work
- [x] QA'd all new/changed functionality manually
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-08-29 15:37:05 +00:00
|
|
|
|
selectedScriptId?: number;
|
2025-10-01 17:15:30 +00:00
|
|
|
|
selectedScriptDetails?: PartialOrFullHostScript | IPaginatedListScript | null;
|
2024-11-06 17:48:11 +00:00
|
|
|
|
selectedScriptContent?: string;
|
|
|
|
|
|
isLoadingScriptContent?: boolean;
|
|
|
|
|
|
isScriptContentError?: Error | null;
|
|
|
|
|
|
isHidden?: boolean;
|
|
|
|
|
|
onClickRunDetails?: (scriptExecutionId: string) => void;
|
2025-02-27 15:53:34 +00:00
|
|
|
|
teamIdForApi?: number;
|
2025-04-28 23:32:41 +00:00
|
|
|
|
suppressSecondaryActions?: boolean;
|
|
|
|
|
|
customPrimaryButtons?: React.ReactNode;
|
2024-11-06 17:48:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ScriptDetailsModal = ({
|
|
|
|
|
|
onCancel,
|
2025-10-01 17:15:30 +00:00
|
|
|
|
onClose,
|
2024-11-06 17:48:11 +00:00
|
|
|
|
onDelete,
|
2025-10-01 17:15:30 +00:00
|
|
|
|
onClickRun,
|
2024-11-06 17:48:11 +00:00
|
|
|
|
runScriptHelpText = false,
|
|
|
|
|
|
showHostScriptActions = false,
|
|
|
|
|
|
hostTeamId,
|
UI: Batch script run detail page (#32333)
## For #31226
New features:
- Dynamic header for each possible state of a batch script run: Started,
Scheduled, and Finished (corresponds to tabs at
`/controls/scripts/progress`
- Unique tabs for each possible status of hosts targeted by a batch
script run: Ran, Errored, Pending, Incompatible, Canceled.
- Within each tab, sortable, paginated host results with output preview
and execution time.
- View script/run details, cancel a batch, view manage hosts page
filtered for the script batch run and a status.
- Global script batch runs activities and and Scripts progress rows now
navigate to this details page.
Cleanups and improvements:
- Expand tab count badge options using “alert”/“pending” variants across
hosts, policies, and query results.
- Misc cleanups and improvements

- [x] Changes file added for user-visible changes in `changes/`,
- [x] Updated automated tests - new tests tracked for follow-up work
- [x] QA'd all new/changed functionality manually
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-08-29 15:37:05 +00:00
|
|
|
|
selectedScriptId,
|
2024-11-06 17:48:11 +00:00
|
|
|
|
selectedScriptDetails,
|
|
|
|
|
|
selectedScriptContent,
|
|
|
|
|
|
isLoadingScriptContent,
|
|
|
|
|
|
isScriptContentError,
|
|
|
|
|
|
isHidden = false,
|
|
|
|
|
|
onClickRunDetails,
|
2025-02-27 15:53:34 +00:00
|
|
|
|
teamIdForApi,
|
2025-04-28 23:32:41 +00:00
|
|
|
|
suppressSecondaryActions = false,
|
|
|
|
|
|
customPrimaryButtons,
|
2024-11-06 17:48:11 +00:00
|
|
|
|
}: 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
Technician role FE changes (#39494)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #38630
## Testing
- [x] QA'd all new/changed functionality manually
Screenshots below were taken with a **Team Technician** user. Same
changes apply for a **Global Technician**.
#### Controls > OS settings > Disk encryption
- Shows table without controls below.
- Shows empty state (doesn't allow to turn it on).
<img width="1915" height="886" alt="Screenshot 2026-02-10 at 12 24
25 PM"
src="https://github.com/user-attachments/assets/3f44d338-e728-4eb2-ad93-e30844201b52"
/>
<img width="1913" height="907" alt="Screenshot 2026-02-10 at 12 31
38 PM"
src="https://github.com/user-attachments/assets/71706e9e-0540-4c25-b5c0-3f7ccff3ba5a"
/>
#### Controls > OS settings > Custom settings
- Changed description to say **View configuration profiles that apply
custom settings.** instead of **Create and upload configuration profiles
to apply custom settings.**.
- **Add profile** not shown within table header.
- Trash can icon not shown when hovering over a row within the table.
- **Add profile** card not shown on empty state. Instead, "No
configuration profiles have been added." is shown.
<img width="1911" height="729" alt="Screenshot 2026-02-10 at 12 24
39 PM"
src="https://github.com/user-attachments/assets/aa68cbaf-4772-402d-9288-b4be2ddd3250"
/>
<img width="1912" height="650" alt="Screenshot 2026-02-10 at 12 28
48 PM"
src="https://github.com/user-attachments/assets/6a186172-b01f-4314-bb50-4cb533e13bce"
/>
#### Controls > Scripts > Library
- **Add script** not shown within table header.
- No actions shown when hovering over a table row.
- Can view script by clicking on a table row.
- Removed **To run the script across multiple hosts, add a policy
automation on the Policies page** line below **To run this script on a
host, go to the Hosts page and select a host.**.
- Updated copy to `To run this script on a host, go to the Hosts page
and select a host. Then, click Actions > Run script.`
<img width="1912" height="772" alt="Screenshot 2026-02-10 at 12 25
46 PM"
src="https://github.com/user-attachments/assets/83fbc1ec-3a6e-4bb5-865e-b5e7faef1e37"
/>
<img width="1732" height="761" alt="Screenshot 2026-02-11 at 3 50 33 PM"
src="https://github.com/user-attachments/assets/6dda97d7-fde2-4bcd-94b3-fa7368c65528"
/>
#### Labels
Can add label and filter by label
<img width="160" height="247" alt="Screenshot 2026-02-10 at 12 51 24 PM"
src="https://github.com/user-attachments/assets/ed63b708-27f8-4363-9d4f-9a7b0bf82b21"
/>
<img width="1901" height="856" alt="Screenshot 2026-02-10 at 12 35
07 PM"
src="https://github.com/user-attachments/assets/c2ef5e21-03ab-4955-a22f-cd6ca32f3179"
/>
<img width="1903" height="937" alt="Screenshot 2026-02-10 at 12 36
11 PM"
src="https://github.com/user-attachments/assets/d9d9f3bc-4d71-4c4b-902a-455eec9e057c"
/>
Can edit/delete labels created by themselves.
NOTE: my technician user ID is 37 - note that the **x** label belongs to
a different user id, while the second label belongs to ID 37, therefore
it can be edited and deleted.
<img width="1915" height="1152" alt="Screenshot 2026-02-10 at 12 38
29 PM"
src="https://github.com/user-attachments/assets/21f44c11-4e2d-456b-8547-90936b5d7602"
/>
<img width="1911" height="1154" alt="Screenshot 2026-02-10 at 12 38
42 PM"
src="https://github.com/user-attachments/assets/f9f7ea30-11b2-4d2d-9d71-de7299e4b451"
/>
Can delete manual label from host
https://github.com/user-attachments/assets/b64ba6dd-3f54-4dcd-9c57-7bede65122da
#### Host details
Can run scripts and view their results
<img width="1908" height="472" alt="Screenshot 2026-02-10 at 12 52
33 PM"
src="https://github.com/user-attachments/assets/d1e40339-ec52-47ff-bc53-c311498ffe80"
/>
<img width="1882" height="716" alt="Screenshot 2026-02-10 at 12 52
40 PM"
src="https://github.com/user-attachments/assets/dd0c2ec3-8cb8-4835-9c6d-f731a7434637"
/>
<img width="1915" height="718" alt="Screenshot 2026-02-10 at 12 52
48 PM"
src="https://github.com/user-attachments/assets/5e7a73e0-ac5b-4d38-b635-770f53dea9e3"
/>
<img width="1914" height="718" alt="Screenshot 2026-02-10 at 12 52
55 PM"
src="https://github.com/user-attachments/assets/b199c796-66b1-46bc-b2b5-fd35e8aa7a7c"
/>
Can run query associated to host as a live query
https://github.com/user-attachments/assets/7aea6f63-e443-4fa0-87dc-48bef84efa2f
#### Software
Doesn't show trash can icon on software installer card, just the
download one.
<img width="1423" height="838" alt="Screenshot 2026-02-10 at 1 33 53 PM"
src="https://github.com/user-attachments/assets/3a55c226-0bba-43ac-8594-7b5ac0a3684a"
/>
Can install/uninstall software on a host. Note that **Add software**
button is hidden (technicians can't add software).
<img width="1378" height="277" alt="Screenshot 2026-02-10 at 3 08 55 PM"
src="https://github.com/user-attachments/assets/bf413467-2071-48b6-b62b-f3a721b6057c"
/>
#### Queries
- Can run inherited queries on all hosts
https://github.com/user-attachments/assets/09f07e6b-a8c1-453e-81fd-4deb16005836
- Can run team queries on all hosts
https://github.com/user-attachments/assets/18b62dea-e159-40ea-b0ed-1d96b6bd40e7
- Can't manage automations or add queries (buttons are not shown at the
top-right corner)
#### Policies
Same as Queries
https://github.com/user-attachments/assets/2c24514a-2ae0-47a6-b631-6f9e48fc7b9c
#### Protected routes
Tested that I can't access routes that have restricted functionality for
this role, such as:
- **/controls/os-updates**, **/controls/setup-experience** and
**/controls/os-settings/certificates** => redirects to
**/controls/os-settings** ✅
- **/controls/scripts/progress** => redirects to
**/controls/scripts/library** ✅
- **/queries/new** and **/software/add/*** => renders access denied page
✅
---------
Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
2026-02-11 21:38:41 +00:00
|
|
|
|
const {
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
isGlobalAdmin,
|
|
|
|
|
|
isAnyTeamAdmin,
|
|
|
|
|
|
isGlobalMaintainer,
|
|
|
|
|
|
isAnyTeamMaintainer,
|
|
|
|
|
|
isTeamTechnician,
|
|
|
|
|
|
isGlobalTechnician,
|
|
|
|
|
|
} = useContext(AppContext);
|
|
|
|
|
|
|
|
|
|
|
|
const isTechnician = !!isTeamTechnician || !!isGlobalTechnician;
|
|
|
|
|
|
|
|
|
|
|
|
const canRunScripts = !!(
|
|
|
|
|
|
isGlobalAdmin ||
|
|
|
|
|
|
isAnyTeamAdmin ||
|
|
|
|
|
|
isGlobalMaintainer ||
|
|
|
|
|
|
isAnyTeamMaintainer
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2024-11-06 17:48:11 +00:00
|
|
|
|
const { renderFlash } = useContext(NotificationContext);
|
|
|
|
|
|
|
2025-04-28 23:32:41 +00:00
|
|
|
|
// handle multiple possibilities for `selectedScriptDetails`
|
|
|
|
|
|
let scriptId: number | null = null;
|
UI: Batch script run detail page (#32333)
## For #31226
New features:
- Dynamic header for each possible state of a batch script run: Started,
Scheduled, and Finished (corresponds to tabs at
`/controls/scripts/progress`
- Unique tabs for each possible status of hosts targeted by a batch
script run: Ran, Errored, Pending, Incompatible, Canceled.
- Within each tab, sortable, paginated host results with output preview
and execution time.
- View script/run details, cancel a batch, view manage hosts page
filtered for the script batch run and a status.
- Global script batch runs activities and and Scripts progress rows now
navigate to this details page.
Cleanups and improvements:
- Expand tab count badge options using “alert”/“pending” variants across
hosts, policies, and query results.
- Misc cleanups and improvements

- [x] Changes file added for user-visible changes in `changes/`,
- [x] Updated automated tests - new tests tracked for follow-up work
- [x] QA'd all new/changed functionality manually
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-08-29 15:37:05 +00:00
|
|
|
|
if (selectedScriptId) {
|
|
|
|
|
|
scriptId = selectedScriptId;
|
|
|
|
|
|
} else if (selectedScriptDetails) {
|
2025-04-28 23:32:41 +00:00
|
|
|
|
if ("script_id" in selectedScriptDetails) {
|
|
|
|
|
|
scriptId = selectedScriptDetails.script_id;
|
|
|
|
|
|
} else if ("id" in selectedScriptDetails) {
|
|
|
|
|
|
scriptId = selectedScriptDetails.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-06 17:48:11 +00:00
|
|
|
|
const {
|
|
|
|
|
|
data: scriptContent,
|
|
|
|
|
|
error: isSelectedScriptContentError,
|
|
|
|
|
|
isLoading: isLoadingSelectedScriptContent,
|
2026-04-11 00:49:52 +00:00
|
|
|
|
} = useQuery<string, Error>(
|
2025-04-28 23:32:41 +00:00
|
|
|
|
["scriptContent", scriptId],
|
2024-11-06 17:48:11 +00:00
|
|
|
|
() =>
|
2025-04-28 23:32:41 +00:00
|
|
|
|
scriptId
|
2024-11-08 14:22:49 +00:00
|
|
|
|
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
2025-04-28 23:32:41 +00:00
|
|
|
|
scriptAPI.downloadScript(scriptId)
|
2024-11-06 17:48:11 +00:00
|
|
|
|
: Promise.resolve(null),
|
|
|
|
|
|
{
|
|
|
|
|
|
refetchOnWindowFocus: false,
|
2025-04-28 23:32:41 +00:00
|
|
|
|
enabled: !selectedScriptContent && !!scriptId,
|
2024-11-06 17:48:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
);
|
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 {
|
2026-04-11 00:49:52 +00:00
|
|
|
|
const content = selectedScriptContent || scriptContent || "";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
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) => {
|
2025-10-01 17:15:30 +00:00
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "showRunDetails": {
|
|
|
|
|
|
if (script.last_execution?.execution_id) {
|
|
|
|
|
|
onClickRunDetails &&
|
|
|
|
|
|
onClickRunDetails(script.last_execution?.execution_id);
|
2024-11-06 17:48:11 +00:00
|
|
|
|
}
|
2025-10-01 17:15:30 +00:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case "run": {
|
|
|
|
|
|
// should always be present if these actions are visible
|
|
|
|
|
|
onClickRun && onClickRun(script);
|
|
|
|
|
|
break;
|
2024-11-06 17:48:11 +00:00
|
|
|
|
}
|
2025-10-01 17:15:30 +00:00
|
|
|
|
default: // do nothing
|
2024-11-06 17:48:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-10-01 17:15:30 +00:00
|
|
|
|
[onClickRunDetails, onClickRun]
|
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 = () => {
|
|
|
|
|
|
return (
|
2024-12-03 14:09:53 +00:00
|
|
|
|
<ModalFooter
|
|
|
|
|
|
isTopScrolling={isTopScrolling}
|
|
|
|
|
|
secondaryButtons={
|
2025-04-28 23:32:41 +00:00
|
|
|
|
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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)
|
2024-12-03 14:09:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
primaryButtons={
|
2025-04-28 23:32:41 +00:00
|
|
|
|
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,
|
2024-12-03 14:09:53 +00:00
|
|
|
|
selectedScriptDetails as IHostScript
|
2025-04-28 23:32:41 +00:00
|
|
|
|
)}
|
|
|
|
|
|
menuPlacement="top"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-23 15:59:18 +00:00
|
|
|
|
<Button onClick={onCancel}>Close</Button>
|
2025-04-28 23:32:41 +00:00
|
|
|
|
</>
|
|
|
|
|
|
)
|
2024-12-03 14:09:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
/>
|
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}
|
|
|
|
|
|
>
|
2025-04-28 23:32:41 +00:00
|
|
|
|
<Textarea label="Script content:" variant="code">
|
2024-11-06 17:48:11 +00:00
|
|
|
|
{scriptContent}
|
|
|
|
|
|
</Textarea>
|
|
|
|
|
|
{runScriptHelpText && (
|
Technician role FE changes (#39494)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #38630
## Testing
- [x] QA'd all new/changed functionality manually
Screenshots below were taken with a **Team Technician** user. Same
changes apply for a **Global Technician**.
#### Controls > OS settings > Disk encryption
- Shows table without controls below.
- Shows empty state (doesn't allow to turn it on).
<img width="1915" height="886" alt="Screenshot 2026-02-10 at 12 24
25 PM"
src="https://github.com/user-attachments/assets/3f44d338-e728-4eb2-ad93-e30844201b52"
/>
<img width="1913" height="907" alt="Screenshot 2026-02-10 at 12 31
38 PM"
src="https://github.com/user-attachments/assets/71706e9e-0540-4c25-b5c0-3f7ccff3ba5a"
/>
#### Controls > OS settings > Custom settings
- Changed description to say **View configuration profiles that apply
custom settings.** instead of **Create and upload configuration profiles
to apply custom settings.**.
- **Add profile** not shown within table header.
- Trash can icon not shown when hovering over a row within the table.
- **Add profile** card not shown on empty state. Instead, "No
configuration profiles have been added." is shown.
<img width="1911" height="729" alt="Screenshot 2026-02-10 at 12 24
39 PM"
src="https://github.com/user-attachments/assets/aa68cbaf-4772-402d-9288-b4be2ddd3250"
/>
<img width="1912" height="650" alt="Screenshot 2026-02-10 at 12 28
48 PM"
src="https://github.com/user-attachments/assets/6a186172-b01f-4314-bb50-4cb533e13bce"
/>
#### Controls > Scripts > Library
- **Add script** not shown within table header.
- No actions shown when hovering over a table row.
- Can view script by clicking on a table row.
- Removed **To run the script across multiple hosts, add a policy
automation on the Policies page** line below **To run this script on a
host, go to the Hosts page and select a host.**.
- Updated copy to `To run this script on a host, go to the Hosts page
and select a host. Then, click Actions > Run script.`
<img width="1912" height="772" alt="Screenshot 2026-02-10 at 12 25
46 PM"
src="https://github.com/user-attachments/assets/83fbc1ec-3a6e-4bb5-865e-b5e7faef1e37"
/>
<img width="1732" height="761" alt="Screenshot 2026-02-11 at 3 50 33 PM"
src="https://github.com/user-attachments/assets/6dda97d7-fde2-4bcd-94b3-fa7368c65528"
/>
#### Labels
Can add label and filter by label
<img width="160" height="247" alt="Screenshot 2026-02-10 at 12 51 24 PM"
src="https://github.com/user-attachments/assets/ed63b708-27f8-4363-9d4f-9a7b0bf82b21"
/>
<img width="1901" height="856" alt="Screenshot 2026-02-10 at 12 35
07 PM"
src="https://github.com/user-attachments/assets/c2ef5e21-03ab-4955-a22f-cd6ca32f3179"
/>
<img width="1903" height="937" alt="Screenshot 2026-02-10 at 12 36
11 PM"
src="https://github.com/user-attachments/assets/d9d9f3bc-4d71-4c4b-902a-455eec9e057c"
/>
Can edit/delete labels created by themselves.
NOTE: my technician user ID is 37 - note that the **x** label belongs to
a different user id, while the second label belongs to ID 37, therefore
it can be edited and deleted.
<img width="1915" height="1152" alt="Screenshot 2026-02-10 at 12 38
29 PM"
src="https://github.com/user-attachments/assets/21f44c11-4e2d-456b-8547-90936b5d7602"
/>
<img width="1911" height="1154" alt="Screenshot 2026-02-10 at 12 38
42 PM"
src="https://github.com/user-attachments/assets/f9f7ea30-11b2-4d2d-9d71-de7299e4b451"
/>
Can delete manual label from host
https://github.com/user-attachments/assets/b64ba6dd-3f54-4dcd-9c57-7bede65122da
#### Host details
Can run scripts and view their results
<img width="1908" height="472" alt="Screenshot 2026-02-10 at 12 52
33 PM"
src="https://github.com/user-attachments/assets/d1e40339-ec52-47ff-bc53-c311498ffe80"
/>
<img width="1882" height="716" alt="Screenshot 2026-02-10 at 12 52
40 PM"
src="https://github.com/user-attachments/assets/dd0c2ec3-8cb8-4835-9c6d-f731a7434637"
/>
<img width="1915" height="718" alt="Screenshot 2026-02-10 at 12 52
48 PM"
src="https://github.com/user-attachments/assets/5e7a73e0-ac5b-4d38-b635-770f53dea9e3"
/>
<img width="1914" height="718" alt="Screenshot 2026-02-10 at 12 52
55 PM"
src="https://github.com/user-attachments/assets/b199c796-66b1-46bc-b2b5-fd35e8aa7a7c"
/>
Can run query associated to host as a live query
https://github.com/user-attachments/assets/7aea6f63-e443-4fa0-87dc-48bef84efa2f
#### Software
Doesn't show trash can icon on software installer card, just the
download one.
<img width="1423" height="838" alt="Screenshot 2026-02-10 at 1 33 53 PM"
src="https://github.com/user-attachments/assets/3a55c226-0bba-43ac-8594-7b5ac0a3684a"
/>
Can install/uninstall software on a host. Note that **Add software**
button is hidden (technicians can't add software).
<img width="1378" height="277" alt="Screenshot 2026-02-10 at 3 08 55 PM"
src="https://github.com/user-attachments/assets/bf413467-2071-48b6-b62b-f3a721b6057c"
/>
#### Queries
- Can run inherited queries on all hosts
https://github.com/user-attachments/assets/09f07e6b-a8c1-453e-81fd-4deb16005836
- Can run team queries on all hosts
https://github.com/user-attachments/assets/18b62dea-e159-40ea-b0ed-1d96b6bd40e7
- Can't manage automations or add queries (buttons are not shown at the
top-right corner)
#### Policies
Same as Queries
https://github.com/user-attachments/assets/2c24514a-2ae0-47a6-b631-6f9e48fc7b9c
#### Protected routes
Tested that I can't access routes that have restricted functionality for
this role, such as:
- **/controls/os-updates**, **/controls/setup-experience** and
**/controls/os-settings/certificates** => redirects to
**/controls/os-settings** ✅
- **/controls/scripts/progress** => redirects to
**/controls/scripts/library** ✅
- **/queries/new** and **/software/add/*** => renders access denied page
✅
---------
Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
2026-02-11 21:38:41 +00:00
|
|
|
|
<RunScriptHelpText
|
|
|
|
|
|
className="form-field__help-text"
|
|
|
|
|
|
isTechnician={isTechnician}
|
|
|
|
|
|
canRunScripts={canRunScripts}
|
|
|
|
|
|
teamId={teamIdForApi}
|
|
|
|
|
|
/>
|
2024-11-06 17:48:11 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
className={baseClass}
|
|
|
|
|
|
title={selectedScriptDetails?.name || "Script details"}
|
|
|
|
|
|
width="large"
|
2025-10-01 17:15:30 +00:00
|
|
|
|
onExit={onClose ?? onCancel}
|
2024-11-06 17:48:11 +00:00
|
|
|
|
isHidden={isHidden}
|
|
|
|
|
|
>
|
2026-03-10 22:30:55 +00:00
|
|
|
|
{renderContent()}
|
|
|
|
|
|
{shouldShowFooter && renderFooter()}
|
2024-11-06 17:48:11 +00:00
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default ScriptDetailsModal;
|