fleet/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/SelectQueryModal.tsx
Ian Littman 2891904f31
🤖 Switch InputField + InputFieldWithIcon JSX components to TS, add more test coverage, fix Storybook build (#43307)
Zed + Opus 4.6; prompt: Convert the InputField JSX component to
TypeScript and remove the ts-ignore directives that we no longer need
after doing so.

- [x] Changes file added
- [x] Automated tests updated
2026-04-09 08:41:48 -05:00

252 lines
6.6 KiB
TypeScript

import React, { useState, useCallback, useContext } from "react";
import { useQuery } from "react-query";
import { filter, includes } from "lodash";
import { InjectedRouter } from "react-router";
import PATHS from "router/paths";
import permissions from "utilities/permissions";
import { AppContext } from "context/app";
import { QueryContext } from "context/query";
import queryAPI from "services/entities/queries";
// @ts-ignore
import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon";
import Button from "components/buttons/Button";
import Modal from "components/Modal";
import DataError from "components/DataError";
import {
IListQueriesResponse,
IQueryKeyQueriesLoadAll,
ISchedulableQuery,
} from "interfaces/schedulable_query";
import { API_ALL_TEAMS_ID } from "interfaces/team";
import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target";
import { getPathWithQueryParams } from "utilities/url";
export interface ISelectQueryModalProps {
onCancel: () => void;
isOnlyObserver?: boolean;
hostId: number;
hostTeamId: number | null;
router: InjectedRouter; // v3
currentTeamId: number | undefined;
}
const baseClass = "select-query-modal";
const SelectQueryModal = ({
onCancel,
isOnlyObserver,
hostId,
hostTeamId,
router,
currentTeamId,
}: ISelectQueryModalProps): JSX.Element => {
const { setSelectedQueryTargetsByType } = useContext(QueryContext);
const { data: queries, error: queriesErr } = useQuery<
IListQueriesResponse,
Error,
ISchedulableQuery[],
IQueryKeyQueriesLoadAll[]
>(
[
{
scope: "queries",
teamId: hostTeamId || API_ALL_TEAMS_ID,
mergeInherited: hostTeamId !== API_ALL_TEAMS_ID,
},
],
({ queryKey }) => queryAPI.loadAll(queryKey[0]),
{
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retry: false,
select: (data: IListQueriesResponse) => data.queries,
}
);
const onQueryHostCustom = () => {
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
router.push(
getPathWithQueryParams(PATHS.NEW_REPORT, {
host_id: hostId,
fleet_id: currentTeamId,
})
);
};
const onQueryHostSaved = (selectedQuery: ISchedulableQuery) => {
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
router.push(
getPathWithQueryParams(PATHS.EDIT_REPORT(selectedQuery.id), {
host_id: hostId,
fleet_id: currentTeamId,
})
);
};
let queriesAvailableToRun = queries;
const { currentUser, isObserverPlus } = useContext(AppContext);
/* Context team id might be different that host's team id
Observer plus must be checked against host's team id */
const isHostsTeamObserverPlus = currentUser
? permissions.isObserverPlus(currentUser, hostTeamId)
: false;
const [queriesFilter, setQueriesFilter] = useState("");
if (isOnlyObserver && !isObserverPlus && !isHostsTeamObserverPlus) {
queriesAvailableToRun =
queries?.filter((query) => query.observer_can_run === true) || [];
}
const getQueries = () => {
if (!queriesFilter) {
return queriesAvailableToRun;
}
const lowerQueryFilter = queriesFilter.toLowerCase();
return filter(queriesAvailableToRun, (query) => {
if (!query.name) {
return false;
}
const lowerQueryName = query.name.toLowerCase();
return includes(lowerQueryName, lowerQueryFilter);
});
};
const onFilterQueries = useCallback(
(filterString: string): void => {
setQueriesFilter(filterString);
},
[setQueriesFilter]
);
const queriesFiltered = getQueries();
const queriesCount = queriesFiltered?.length || 0;
const renderDescription = (): JSX.Element => {
return (
<div className={`${baseClass}__description`}>
Choose a report to run on this host
{(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && (
<>
{" "}
or{" "}
<Button variant="text-link" onClick={onQueryHostCustom}>
create your own report
</Button>
</>
)}
.
</div>
);
};
const renderQueries = (): JSX.Element => {
if (queriesErr) {
return <DataError />;
}
if (!queriesFilter && queriesCount === 0) {
return (
<div className={`${baseClass}__no-queries`}>
<span className="info__header">You have no saved reports.</span>
<span className="info__data">
Expecting to see reports? Try again in a few seconds as the system
catches up.
</span>
</div>
);
}
if (queriesCount > 0) {
const queryList =
queriesFiltered?.map((query) => {
return (
<Button
key={query.id}
variant="unstyled-modal-query"
className={`${baseClass}__modal-query-button`}
onClick={() => onQueryHostSaved(query)}
>
<>
<span className="info__header">{query.name}</span>
{query.description && (
<span className="info__data">{query.description}</span>
)}
</>
</Button>
);
}) || [];
return (
<>
<InputFieldWithIcon
name="query-filter"
onChange={onFilterQueries}
placeholder="Filter reports"
value={queriesFilter}
autofocus
iconSvg="search"
/>
<div className={`${baseClass}__query-selection`}>{queryList}</div>
</>
);
}
if (queriesFilter && queriesCount === 0) {
return (
<>
<div className={`${baseClass}__filter-queries`}>
<InputFieldWithIcon
name="query-filter"
onChange={onFilterQueries}
placeholder="Filter reports"
value={queriesFilter}
autofocus
iconSvg="search"
/>
</div>
<div className={`${baseClass}__no-queries`}>
<span className="info__header">
No reports match the current search criteria.
</span>
<span className="info__data">
Expecting to see reports? Try again in a few seconds as the system
catches up.
</span>
</div>
</>
);
}
return <></>;
};
return (
<Modal
title="Select a report"
onExit={onCancel}
onEnter={onCancel}
className={baseClass}
width="large"
>
{renderDescription()}
{renderQueries()}
</Modal>
);
};
export default SelectQueryModal;