mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
<!-- 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>
228 lines
6.5 KiB
TypeScript
228 lines
6.5 KiB
TypeScript
import React, { useCallback, useContext, useRef, useState } from "react";
|
|
import { AxiosError } from "axios";
|
|
import { useQuery } from "react-query";
|
|
|
|
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
|
import PATHS from "router/paths";
|
|
|
|
import { AppContext } from "context/app";
|
|
|
|
import { IScript } from "interfaces/script";
|
|
import scriptAPI, {
|
|
IListScriptsQueryKey,
|
|
IScriptsResponse,
|
|
} from "services/entities/scripts";
|
|
|
|
import DataError from "components/DataError";
|
|
import InfoBanner from "components/InfoBanner";
|
|
import Spinner from "components/Spinner";
|
|
import Pagination from "components/Pagination";
|
|
import SectionHeader from "components/SectionHeader";
|
|
import Card from "components/Card";
|
|
|
|
import UploadList from "../../../components/UploadList";
|
|
import DeleteScriptModal from "../../components/DeleteScriptModal";
|
|
import EditScriptModal from "../../components/EditScriptModal";
|
|
import ScriptUploadModal from "../../components/ScriptUploadModal";
|
|
import ScriptListHeading from "../../components/ScriptListHeading";
|
|
import ScriptListItem from "../../components/ScriptListItem";
|
|
import ScriptUploader from "../../components/ScriptUploader";
|
|
import { IScriptsCommonProps } from "../../ScriptsNavItems";
|
|
|
|
const baseClass = "script-library";
|
|
|
|
const SCRIPTS_PER_PAGE = 10;
|
|
const DEFAULT_PAGE = 0;
|
|
|
|
export type IScriptLibraryProps = IScriptsCommonProps;
|
|
|
|
const ScriptLibrary = ({ router, teamId, location }: IScriptLibraryProps) => {
|
|
const currentPage = location.query.page
|
|
? parseInt(location.query.page, 10)
|
|
: DEFAULT_PAGE;
|
|
|
|
const { isPremiumTier, isGlobalTechnician, isTeamTechnician } = useContext(
|
|
AppContext
|
|
);
|
|
|
|
const isTechnician = isGlobalTechnician || isTeamTechnician;
|
|
|
|
const [showDeleteScriptModal, setShowDeleteScriptModal] = useState(false);
|
|
const [showEditScriptModal, setShowEditScriptModal] = useState(false);
|
|
const [showAddScriptModal, setShowAddScriptModal] = useState(false);
|
|
|
|
const selectedScript = useRef<IScript | null>(null);
|
|
|
|
const {
|
|
data: { scripts, meta } = {},
|
|
isLoading,
|
|
isError,
|
|
refetch: refetchScripts,
|
|
} = useQuery<
|
|
IScriptsResponse,
|
|
AxiosError,
|
|
IScriptsResponse,
|
|
IListScriptsQueryKey[]
|
|
>(
|
|
[
|
|
{
|
|
scope: "scripts",
|
|
team_id: teamId,
|
|
page: currentPage,
|
|
per_page: SCRIPTS_PER_PAGE,
|
|
},
|
|
],
|
|
({ queryKey: [{ team_id, page, per_page }] }) =>
|
|
scriptAPI.getScripts({ team_id, page, per_page }),
|
|
{
|
|
...DEFAULT_USE_QUERY_OPTIONS,
|
|
staleTime: 3000,
|
|
}
|
|
);
|
|
|
|
// pagination controls
|
|
const path = PATHS.CONTROLS_SCRIPTS_LIBRARY;
|
|
const queryString = isPremiumTier ? `?team_id=${teamId}&` : "?";
|
|
const onPrevPage = useCallback(() => {
|
|
router.push(path.concat(`${queryString}page=${currentPage - 1}`));
|
|
}, [router, path, currentPage, queryString]);
|
|
const onNextPage = useCallback(() => {
|
|
router.push(path.concat(`${queryString}page=${currentPage + 1}`));
|
|
}, [router, path, currentPage, queryString]);
|
|
|
|
const { config } = useContext(AppContext);
|
|
if (!config) return null;
|
|
|
|
const onClickScript = (script: IScript) => {
|
|
selectedScript.current = script;
|
|
setShowEditScriptModal(true);
|
|
};
|
|
|
|
const onEditScript = (script: IScript) => {
|
|
selectedScript.current = script;
|
|
setShowEditScriptModal(true);
|
|
};
|
|
|
|
const onExitEditScript = () => {
|
|
selectedScript.current = null;
|
|
setShowEditScriptModal(false);
|
|
};
|
|
|
|
const onClickDelete = (script: IScript) => {
|
|
selectedScript.current = script;
|
|
setShowDeleteScriptModal(true);
|
|
};
|
|
|
|
const onCancelDelete = () => {
|
|
setShowDeleteScriptModal(false);
|
|
selectedScript.current = null;
|
|
};
|
|
|
|
const onDeleteScript = () => {
|
|
selectedScript.current = null;
|
|
setShowDeleteScriptModal(false);
|
|
refetchScripts();
|
|
};
|
|
|
|
const renderScriptsList = () => {
|
|
if (isLoading) {
|
|
return <Spinner />;
|
|
}
|
|
|
|
if (isError) {
|
|
return <DataError />;
|
|
}
|
|
|
|
if (currentPage === 0 && !scripts?.length) {
|
|
return null;
|
|
}
|
|
|
|
const headingComponent = () => (
|
|
<ScriptListHeading
|
|
onClickAddScript={
|
|
isTechnician ? undefined : () => setShowAddScriptModal(true)
|
|
}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<UploadList
|
|
keyAttribute="id"
|
|
listItems={scripts || []}
|
|
HeadingComponent={headingComponent}
|
|
ListItemComponent={({ listItem }) => (
|
|
<ScriptListItem
|
|
script={listItem}
|
|
onDelete={onClickDelete}
|
|
onClickScript={onClickScript}
|
|
onEdit={onEditScript}
|
|
isTechnician={isTechnician}
|
|
/>
|
|
)}
|
|
/>
|
|
<Pagination
|
|
disablePrev={isLoading || !meta?.has_previous_results}
|
|
disableNext={isLoading || !meta?.has_next_results}
|
|
hidePagination={
|
|
!isLoading && !meta?.has_previous_results && !meta?.has_next_results
|
|
}
|
|
onPrevPage={onPrevPage}
|
|
onNextPage={onNextPage}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const renderScriptsDisabledBanner = () => (
|
|
<InfoBanner color="yellow">
|
|
<div>
|
|
<b>Running scripts is disabled in organization settings.</b> You can
|
|
still manage your library of macOS and Windows scripts below.
|
|
</div>
|
|
</InfoBanner>
|
|
);
|
|
|
|
return (
|
|
<div className={baseClass}>
|
|
<SectionHeader title="Library" alignLeftHeaderVertically />
|
|
{config.server_settings.scripts_disabled && renderScriptsDisabledBanner()}
|
|
{renderScriptsList()}
|
|
{!isLoading &&
|
|
currentPage === 0 &&
|
|
!scripts?.length &&
|
|
(isTechnician ? (
|
|
<Card className="empty-scripts">No scripts uploaded.</Card>
|
|
) : (
|
|
<ScriptUploader onButtonClick={() => setShowAddScriptModal(true)} />
|
|
))}
|
|
{showDeleteScriptModal && selectedScript.current && (
|
|
<DeleteScriptModal
|
|
scriptName={selectedScript.current?.name}
|
|
scriptId={selectedScript.current?.id}
|
|
onCancel={onCancelDelete}
|
|
afterDelete={onDeleteScript}
|
|
/>
|
|
)}
|
|
{showEditScriptModal && selectedScript.current && (
|
|
<EditScriptModal
|
|
scriptId={selectedScript.current.id}
|
|
scriptName={selectedScript.current.name}
|
|
onExit={onExitEditScript}
|
|
/>
|
|
)}
|
|
{showAddScriptModal && (
|
|
<ScriptUploadModal
|
|
currentTeamId={teamId}
|
|
onExit={() => setShowAddScriptModal(false)}
|
|
onSubmit={() => {
|
|
setShowAddScriptModal(false);
|
|
refetchScripts();
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ScriptLibrary;
|