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 #32632 # Details This PR updates the Script Library page in the following ways: * When no scripts are uploaded for a team, it shows the "Add script" UI with a button that opens a new "Add Script" modal * When scripts are uploaded, the "Add script" button is instead added to the header of the scripts list, and clicking it opens that modal # Checklist for submitter If some of the following don't apply, delete the relevant line. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [ ] Added/updated automated tests working on this - [X] QA'd all new/changed functionality manually - [X] Test empty state: go to controls/scripts/library for a team with no scripts. Clicking "upload" button in empty state should open the add script modal. - [X] In the modal, select a .ps1 script. Should not see additional text. - [X] Close modal without uploading. Re-open. File field should be cleared & upload button visible again. - [X] Select a .sh script. Should see additional text about macOS and Linux. - [X] Add script. Make sure script saves and modal closes. - [X] Once script has been added, make sure empty state is gone and "Add script" button is at the top of the list. - [X] Go to /controls/os-settings/custom-settings for a team with no profiles uploaded. Make sure empty state text styles match the empty state for script uploads. - [X] Open modal to add profile. Make sure upload text styles match the script upload modal. - [X] Enable GitOps mode. Go to controls/scripts/library for a team with scripts added. Make sure new "Add script" button is disabled w/ standard tooltip in GitOps mode. Scripts empty state: <img width="697" height="352" alt="image" src="https://github.com/user-attachments/assets/32f0f246-bddb-4bb7-bc39-48d9978de9fa" /> Scripts uploader: <img width="745" height="590" alt="image" src="https://github.com/user-attachments/assets/f82414e2-9318-4543-b5ca-41e759662587" /> Scripts uploader with .sh <img width="750" height="539" alt="image" src="https://github.com/user-attachments/assets/0b989067-921a-4d18-93ed-09aac90fc9cb" /> Scripts table: <img width="686" height="256" alt="image" src="https://github.com/user-attachments/assets/848f1b56-6e9e-48d4-9a03-6fdf5427301e" /> Profiles empty state: <img width="700" height="377" alt="image" src="https://github.com/user-attachments/assets/8f92bcd9-2215-41f6-a540-4774f7e9542b" /> Profiles uploader: <img width="707" height="682" alt="image" src="https://github.com/user-attachments/assets/eef216af-3447-48e7-882a-e42e888e1c17" />
216 lines
6.2 KiB
TypeScript
216 lines
6.2 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 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 } = useContext(AppContext);
|
|
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 onUploadScript = () => {
|
|
refetchScripts();
|
|
};
|
|
|
|
const renderScriptsList = () => {
|
|
if (isLoading) {
|
|
return <Spinner />;
|
|
}
|
|
|
|
if (isError) {
|
|
return <DataError />;
|
|
}
|
|
|
|
if (currentPage === 0 && !scripts?.length) {
|
|
return null;
|
|
}
|
|
|
|
const headingComponent = () => (
|
|
<ScriptListHeading onClickAddScript={() => setShowAddScriptModal(true)} />
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<UploadList
|
|
keyAttribute="id"
|
|
listItems={scripts || []}
|
|
HeadingComponent={headingComponent}
|
|
ListItemComponent={({ listItem }) => (
|
|
<ScriptListItem
|
|
script={listItem}
|
|
onDelete={onClickDelete}
|
|
onClickScript={onClickScript}
|
|
onEdit={onEditScript}
|
|
/>
|
|
)}
|
|
/>
|
|
<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 && (
|
|
<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;
|