mirror of
https://github.com/fleetdm/fleet
synced 2026-05-20 23:48:52 +00:00
439 lines
14 KiB
TypeScript
439 lines
14 KiB
TypeScript
import React, { useState, useEffect, useContext } from "react";
|
|
import { useQuery } from "react-query";
|
|
import { useErrorHandler } from "react-error-boundary";
|
|
import { InjectedRouter, Params } from "react-router/lib/Router";
|
|
import { Location } from "history";
|
|
import PATHS from "router/paths";
|
|
|
|
import { AppContext } from "context/app";
|
|
import { NotificationContext } from "context/notification";
|
|
import { QueryContext } from "context/query";
|
|
import useTeamIdParam from "hooks/useTeamIdParam";
|
|
|
|
import debounce from "utilities/debounce";
|
|
import deepDifference from "utilities/deep_difference";
|
|
import { getPathWithQueryParams } from "utilities/url";
|
|
import {
|
|
DEFAULT_QUERY,
|
|
DOCUMENT_TITLE_SUFFIX,
|
|
INVALID_PLATFORMS_FLASH_MESSAGE,
|
|
INVALID_PLATFORMS_REASON,
|
|
} from "utilities/constants";
|
|
import configAPI from "services/entities/config";
|
|
import queryAPI from "services/entities/queries";
|
|
import statusAPI from "services/entities/status";
|
|
import {
|
|
IGetQueryResponse,
|
|
ICreateQueryRequestBody,
|
|
ISchedulableQuery,
|
|
} from "interfaces/schedulable_query";
|
|
import { IConfig } from "interfaces/config";
|
|
import { getErrorReason } from "interfaces/errors";
|
|
|
|
import QuerySidePanel from "components/side_panels/QuerySidePanel";
|
|
import MainContent from "components/MainContent";
|
|
import SidePanelContent from "components/SidePanelContent";
|
|
import CustomLink from "components/CustomLink";
|
|
import BackLink from "components/BackLink";
|
|
import InfoBanner from "components/InfoBanner";
|
|
import EditQueryForm from "./components/EditQueryForm";
|
|
|
|
interface IEditQueryPageProps {
|
|
router: InjectedRouter;
|
|
params: Params;
|
|
location: Location<{ host_id: string; team_id?: string }>;
|
|
}
|
|
|
|
const baseClass = "edit-query-page";
|
|
|
|
const EditQueryPage = ({
|
|
router,
|
|
params: { id: paramsQueryId },
|
|
location,
|
|
}: IEditQueryPageProps): JSX.Element => {
|
|
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
|
|
const hostId = location.query.host_id
|
|
? parseInt(location.query.host_id as string, 10)
|
|
: undefined;
|
|
|
|
const {
|
|
currentTeamName: teamNameForQuery,
|
|
teamIdForApi: apiTeamIdForQuery,
|
|
currentTeamId,
|
|
} = useTeamIdParam({
|
|
location,
|
|
router,
|
|
includeAllTeams: true,
|
|
includeNoTeam: false,
|
|
});
|
|
|
|
const handlePageError = useErrorHandler();
|
|
const {
|
|
isGlobalAdmin,
|
|
isGlobalMaintainer,
|
|
isTeamMaintainerOrTeamAdmin,
|
|
isAnyTeamMaintainerOrTeamAdmin,
|
|
isObserverPlus,
|
|
isAnyTeamObserverPlus,
|
|
config,
|
|
filteredQueriesPath,
|
|
isOnGlobalTeam,
|
|
} = useContext(AppContext);
|
|
const {
|
|
editingExistingQuery,
|
|
selectedOsqueryTable,
|
|
setSelectedOsqueryTable,
|
|
lastEditedQueryName,
|
|
lastEditedQueryDescription,
|
|
lastEditedQueryBody,
|
|
lastEditedQueryObserverCanRun,
|
|
lastEditedQueryFrequency,
|
|
lastEditedQueryAutomationsEnabled,
|
|
lastEditedQueryPlatforms,
|
|
lastEditedQueryLoggingType,
|
|
lastEditedQueryMinOsqueryVersion,
|
|
lastEditedQueryDiscardData,
|
|
setLastEditedQueryId,
|
|
setLastEditedQueryName,
|
|
setLastEditedQueryDescription,
|
|
setLastEditedQueryBody,
|
|
setLastEditedQueryObserverCanRun,
|
|
setLastEditedQueryFrequency,
|
|
setLastEditedQueryAutomationsEnabled,
|
|
setLastEditedQueryLoggingType,
|
|
setLastEditedQueryMinOsqueryVersion,
|
|
setLastEditedQueryPlatforms,
|
|
setLastEditedQueryDiscardData,
|
|
} = useContext(QueryContext);
|
|
const { setConfig, availableTeams, setCurrentTeam } = useContext(AppContext);
|
|
const { renderFlash } = useContext(NotificationContext);
|
|
|
|
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
|
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
|
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
|
|
false
|
|
);
|
|
const [
|
|
showConfirmSaveChangesModal,
|
|
setShowConfirmSaveChangesModal,
|
|
] = useState(false);
|
|
|
|
const { data: appConfig } = useQuery<IConfig, Error, IConfig>(
|
|
["config"],
|
|
() => configAPI.loadAll(),
|
|
{
|
|
select: (data: IConfig) => data,
|
|
onSuccess: (data) => {
|
|
setConfig(data);
|
|
},
|
|
}
|
|
);
|
|
|
|
// disabled on page load so we can control the number of renders
|
|
// else it will re-populate the context on occasion
|
|
const {
|
|
isLoading: isStoredQueryLoading,
|
|
data: storedQuery,
|
|
refetch: refetchStoredQuery,
|
|
} = useQuery<IGetQueryResponse, Error, ISchedulableQuery>(
|
|
["query", queryId],
|
|
() => queryAPI.load(queryId as number),
|
|
{
|
|
enabled: !!queryId && !editingExistingQuery,
|
|
refetchOnWindowFocus: false,
|
|
select: (data) => data.query,
|
|
onSuccess: (returnedQuery) => {
|
|
setLastEditedQueryId(returnedQuery.id);
|
|
setLastEditedQueryName(returnedQuery.name);
|
|
setLastEditedQueryDescription(returnedQuery.description);
|
|
setLastEditedQueryBody(returnedQuery.query);
|
|
setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run);
|
|
setLastEditedQueryFrequency(returnedQuery.interval);
|
|
setLastEditedQueryAutomationsEnabled(returnedQuery.automations_enabled);
|
|
setLastEditedQueryPlatforms(returnedQuery.platform);
|
|
setLastEditedQueryLoggingType(returnedQuery.logging);
|
|
setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version);
|
|
setLastEditedQueryDiscardData(returnedQuery.discard_data);
|
|
},
|
|
onError: (error) => handlePageError(error),
|
|
}
|
|
);
|
|
|
|
/** Pesky bug affecting team level users:
|
|
- Navigating to queries/:id immediately defaults the user to the first team they're on
|
|
with the most permissions, in the URL bar because of useTeamIdParam
|
|
even if the queries/:id entity has a team attached to it
|
|
Hacky fix:
|
|
- Push entity's team id to url for team level users
|
|
*/
|
|
if (
|
|
!isOnGlobalTeam &&
|
|
!isStoredQueryLoading &&
|
|
storedQuery?.team_id &&
|
|
!(storedQuery?.team_id?.toString() === location.query.team_id)
|
|
) {
|
|
router.push(
|
|
getPathWithQueryParams(location.pathname, {
|
|
team_id: storedQuery?.team_id?.toString(),
|
|
})
|
|
);
|
|
}
|
|
|
|
// Used to set host's team in AppContext for RBAC actions
|
|
useEffect(() => {
|
|
if (storedQuery?.team_id) {
|
|
const querysTeam = availableTeams?.find(
|
|
(team) => team.id === storedQuery.team_id
|
|
);
|
|
setCurrentTeam(querysTeam);
|
|
}
|
|
}, [storedQuery]);
|
|
|
|
const detectIsFleetQueryRunnable = () => {
|
|
statusAPI.live_query().catch(() => {
|
|
setIsLiveQueryRunnable(false);
|
|
});
|
|
};
|
|
|
|
/* Observer/Observer+ cannot edit existing query (O+ has access to edit new query to run live),
|
|
Team admin/team maintainer cannot edit existing query,
|
|
reroute edit existing query page (/:queryId/edit) to query report page (/:queryId) */
|
|
useEffect(() => {
|
|
const canEditExistingQuery =
|
|
isGlobalAdmin ||
|
|
isGlobalMaintainer ||
|
|
(isTeamMaintainerOrTeamAdmin && storedQuery?.team_id);
|
|
|
|
if (
|
|
!isStoredQueryLoading && // Confirms teamId for storedQuery before RBAC reroute
|
|
queryId &&
|
|
queryId > 0 &&
|
|
!canEditExistingQuery
|
|
) {
|
|
// Reroute to query report page still maintains query params for live query purposes
|
|
router.push(
|
|
getPathWithQueryParams(PATHS.QUERY_DETAILS(queryId), {
|
|
host_id: location.query.host_id,
|
|
team_id: location.query.team_id,
|
|
})
|
|
);
|
|
}
|
|
}, [queryId, isTeamMaintainerOrTeamAdmin, isStoredQueryLoading]);
|
|
|
|
useEffect(() => {
|
|
detectIsFleetQueryRunnable();
|
|
if (!queryId) {
|
|
setLastEditedQueryId(DEFAULT_QUERY.id);
|
|
setLastEditedQueryName(DEFAULT_QUERY.name);
|
|
setLastEditedQueryDescription(DEFAULT_QUERY.description);
|
|
// Persist lastEditedQueryBody through live query flow instead of resetting to DEFAULT_QUERY.query
|
|
setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run);
|
|
setLastEditedQueryFrequency(DEFAULT_QUERY.interval);
|
|
setLastEditedQueryAutomationsEnabled(DEFAULT_QUERY.automations_enabled);
|
|
setLastEditedQueryLoggingType(DEFAULT_QUERY.logging);
|
|
setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version);
|
|
setLastEditedQueryPlatforms(DEFAULT_QUERY.platform);
|
|
setLastEditedQueryDiscardData(DEFAULT_QUERY.discard_data);
|
|
}
|
|
}, [queryId]);
|
|
|
|
const [isQuerySaving, setIsQuerySaving] = useState(false);
|
|
const [isQueryUpdating, setIsQueryUpdating] = useState(false);
|
|
const [backendValidators, setBackendValidators] = useState<{
|
|
[key: string]: string;
|
|
}>({});
|
|
|
|
// Updates title that shows up on browser tabs
|
|
useEffect(() => {
|
|
// e.g., Editing Discover TLS certificates | Queries | Fleet
|
|
const storedQueryTitleCopy = storedQuery?.name
|
|
? `Editing ${storedQuery.name} | `
|
|
: "";
|
|
document.title = `${storedQueryTitleCopy}Queries | ${DOCUMENT_TITLE_SUFFIX}`;
|
|
// }
|
|
}, [location.pathname, storedQuery?.name]);
|
|
|
|
useEffect(() => {
|
|
setShowOpenSchemaActionText(!isSidebarOpen);
|
|
}, [isSidebarOpen]);
|
|
|
|
const onSubmitNewQuery = debounce(
|
|
async (formData: ICreateQueryRequestBody) => {
|
|
setIsQuerySaving(true);
|
|
try {
|
|
const { query } = await queryAPI.create(formData);
|
|
router.push(
|
|
getPathWithQueryParams(PATHS.QUERY_DETAILS(query.id), {
|
|
team_id: query.team_id,
|
|
})
|
|
);
|
|
renderFlash("success", "Query created!");
|
|
setBackendValidators({});
|
|
} catch (createError: any) {
|
|
if (getErrorReason(createError).includes("already exists")) {
|
|
const teamErrorText =
|
|
teamNameForQuery && apiTeamIdForQuery !== 0
|
|
? `the ${teamNameForQuery} team`
|
|
: "all teams";
|
|
setBackendValidators({
|
|
name: `A query with that name already exists for ${teamErrorText}.`,
|
|
});
|
|
} else {
|
|
renderFlash(
|
|
"error",
|
|
"Something went wrong creating your query. Please try again."
|
|
);
|
|
setBackendValidators({});
|
|
}
|
|
} finally {
|
|
setIsQuerySaving(false);
|
|
}
|
|
}
|
|
);
|
|
|
|
const onUpdateQuery = async (formData: ICreateQueryRequestBody) => {
|
|
if (!queryId) {
|
|
return false;
|
|
}
|
|
|
|
setIsQueryUpdating(true);
|
|
|
|
const updatedQuery = deepDifference(formData, {
|
|
lastEditedQueryName,
|
|
lastEditedQueryDescription,
|
|
lastEditedQueryBody,
|
|
lastEditedQueryObserverCanRun,
|
|
lastEditedQueryFrequency,
|
|
lastEditedQueryAutomationsEnabled,
|
|
lastEditedQueryPlatforms,
|
|
lastEditedQueryLoggingType,
|
|
lastEditedQueryMinOsqueryVersion,
|
|
lastEditedQueryDiscardData,
|
|
});
|
|
|
|
try {
|
|
await queryAPI.update(queryId, updatedQuery);
|
|
renderFlash("success", "Query updated!");
|
|
refetchStoredQuery(); // Required to compare recently saved query to a subsequent save to the query
|
|
} catch (updateError: any) {
|
|
console.error(updateError);
|
|
const reason = getErrorReason(updateError);
|
|
if (reason.includes("Duplicate")) {
|
|
renderFlash("error", "A query with this name already exists.");
|
|
} else if (reason.includes(INVALID_PLATFORMS_REASON)) {
|
|
renderFlash("error", INVALID_PLATFORMS_FLASH_MESSAGE);
|
|
} else {
|
|
renderFlash(
|
|
"error",
|
|
"Something went wrong updating your query. Please try again."
|
|
);
|
|
}
|
|
}
|
|
|
|
setIsQueryUpdating(false);
|
|
setShowConfirmSaveChangesModal(false); // Closes conditionally opened modal when discarding previous results
|
|
|
|
return false;
|
|
};
|
|
|
|
const onOsqueryTableSelect = (tableName: string) => {
|
|
setSelectedOsqueryTable(tableName);
|
|
};
|
|
|
|
const onCloseSchemaSidebar = () => {
|
|
setIsSidebarOpen(false);
|
|
};
|
|
|
|
const onOpenSchemaSidebar = () => {
|
|
setIsSidebarOpen(true);
|
|
};
|
|
|
|
const renderLiveQueryWarning = (): JSX.Element | null => {
|
|
if (isLiveQueryRunnable || config?.server_settings.live_query_disabled) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<InfoBanner color="yellow">
|
|
Fleet is unable to run a live query. Refresh the page or log in again.
|
|
If this keeps happening please{" "}
|
|
<CustomLink
|
|
url="https://github.com/fleetdm/fleet/issues/new/choose"
|
|
text="file an issue"
|
|
newTab
|
|
variant="banner-link"
|
|
/>
|
|
</InfoBanner>
|
|
);
|
|
};
|
|
|
|
// Function instead of constant eliminates race condition
|
|
// Returns to queries details page, manage queries page with filters, or default manage queries page
|
|
const backToQueriesPath = () =>
|
|
queryId
|
|
? getPathWithQueryParams(PATHS.QUERY_DETAILS(queryId), {
|
|
team_id: currentTeamId,
|
|
})
|
|
: filteredQueriesPath ||
|
|
getPathWithQueryParams(PATHS.MANAGE_QUERIES, {
|
|
team_id: currentTeamId,
|
|
});
|
|
|
|
const showSidebar =
|
|
isSidebarOpen &&
|
|
(isGlobalAdmin ||
|
|
isGlobalMaintainer ||
|
|
isAnyTeamMaintainerOrTeamAdmin ||
|
|
isObserverPlus ||
|
|
isAnyTeamObserverPlus);
|
|
|
|
return (
|
|
<>
|
|
<MainContent className={baseClass}>
|
|
<>
|
|
<div className={`${baseClass}__header-links`}>
|
|
<BackLink
|
|
text={queryId ? "Back to report" : "Back to queries"}
|
|
path={backToQueriesPath()}
|
|
/>
|
|
</div>
|
|
<EditQueryForm
|
|
router={router}
|
|
location={location}
|
|
onSubmitNewQuery={onSubmitNewQuery}
|
|
onOsqueryTableSelect={onOsqueryTableSelect}
|
|
onUpdate={onUpdateQuery}
|
|
storedQuery={storedQuery}
|
|
queryIdForEdit={queryId}
|
|
apiTeamIdForQuery={apiTeamIdForQuery}
|
|
currentTeamId={currentTeamId}
|
|
isStoredQueryLoading={isStoredQueryLoading}
|
|
showOpenSchemaActionText={showOpenSchemaActionText}
|
|
onOpenSchemaSidebar={onOpenSchemaSidebar}
|
|
renderLiveQueryWarning={renderLiveQueryWarning}
|
|
backendValidators={backendValidators}
|
|
isQuerySaving={isQuerySaving}
|
|
isQueryUpdating={isQueryUpdating}
|
|
hostId={hostId}
|
|
queryReportsDisabled={
|
|
appConfig?.server_settings.query_reports_disabled
|
|
}
|
|
showConfirmSaveChangesModal={showConfirmSaveChangesModal}
|
|
setShowConfirmSaveChangesModal={setShowConfirmSaveChangesModal}
|
|
/>
|
|
</>
|
|
</MainContent>
|
|
{showSidebar && (
|
|
<SidePanelContent>
|
|
<QuerySidePanel
|
|
onOsqueryTableSelect={onOsqueryTableSelect}
|
|
selectedOsqueryTable={selectedOsqueryTable}
|
|
onClose={onCloseSchemaSidebar}
|
|
/>
|
|
</SidePanelContent>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default EditQueryPage;
|