Fleet UI: Fix new query bugs introduced (#14309)

This commit is contained in:
RachelElysia 2023-10-06 10:03:19 -07:00 committed by GitHub
parent ab50f0f59d
commit 11fc7edc0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 137 additions and 62 deletions

View file

@ -12,7 +12,7 @@ import {
ISelectLabel,
ISelectTeam,
ISelectTargetsEntity,
ISelectedTargets,
ISelectedTargetsForApi,
} from "interfaces/target";
import { ITeam } from "interfaces/team";
@ -48,8 +48,8 @@ interface ISelectTargetsProps {
targetedTeams: ITeam[];
goToQueryEditor: () => void;
goToRunQuery: () => void;
setSelectedTargets:
| React.Dispatch<React.SetStateAction<ITarget[]>> // Used for policies page level useState hook
setSelectedTargets: // TODO: Refactor policy targets to streamline selectedTargets/selectedTargetsByType
| React.Dispatch<React.SetStateAction<ITarget[]>> // Used for policies page level useState hook
| ((value: ITarget[]) => void); // Used for queries app level QueryContext
setTargetedHosts: React.Dispatch<React.SetStateAction<IHost[]>>;
setTargetedLabels: React.Dispatch<React.SetStateAction<ILabel[]>>;
@ -67,7 +67,7 @@ interface ITargetsQueryKey {
scope: string;
query_id?: number | null;
query?: string | null;
selected?: ISelectedTargets | null;
selected?: ISelectedTargetsForApi | null;
}
const DEBOUNCE_DELAY = 500;
@ -381,12 +381,22 @@ const SelectTargets = ({
}
const { targets_count: total, targets_online: online } = counts;
const onlinePercentage = total > 0 ? Math.round((online / total) * 100) : 0;
const onlinePercentage = () => {
if (total === 0) {
return 0;
}
// If at least 1 host is online, displays <1% instead of 0%
const roundPercentage =
Math.round((online / total) * 100) === 0
? "<1"
: Math.round((online / total) * 100) === 0;
return roundPercentage;
};
return (
<>
<span>{total}</span>&nbsp;host{total > 1 ? `s` : ``} targeted&nbsp; (
{onlinePercentage}
{onlinePercentage()}
%&nbsp;
<TooltipWrapper
tipContent={`Hosts are online if they<br /> have recently checked <br />into Fleet.`}

View file

@ -6,7 +6,12 @@ import { DEFAULT_QUERY } from "utilities/constants";
import { DEFAULT_OSQUERY_TABLE, IOsQueryTable } from "interfaces/osquery_table";
import { SelectedPlatformString } from "interfaces/platform";
import { QueryLoggingOption } from "interfaces/schedulable_query";
import { DEFAULT_TARGETS, ITarget } from "interfaces/target";
import {
DEFAULT_TARGETS,
DEFAULT_TARGETS_BY_TYPE,
ISelectedTargetsByType,
ITarget,
} from "interfaces/target";
type Props = {
children: ReactNode;
@ -23,7 +28,8 @@ type InitialStateType = {
lastEditedQueryPlatforms: SelectedPlatformString;
lastEditedQueryMinOsqueryVersion: string;
lastEditedQueryLoggingType: QueryLoggingOption;
selectedQueryTargets: ITarget[];
selectedQueryTargets: ITarget[]; // Mimicks old selectedQueryTargets still used for policies for SelectTargets.tsx and running a live query
selectedQueryTargetsByType: ISelectedTargetsByType; // New format by type for cleaner app wide state
setLastEditedQueryId: (value: number | null) => void;
setLastEditedQueryName: (value: string) => void;
setLastEditedQueryDescription: (value: string) => void;
@ -35,6 +41,7 @@ type InitialStateType = {
setLastEditedQueryLoggingType: (value: string) => void;
setSelectedOsqueryTable: (tableName: string) => void;
setSelectedQueryTargets: (value: ITarget[]) => void;
setSelectedQueryTargetsByType: (value: ISelectedTargetsByType) => void;
};
export type IQueryContext = InitialStateType;
@ -52,6 +59,7 @@ const initialState = {
lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version,
lastEditedQueryLoggingType: DEFAULT_QUERY.logging,
selectedQueryTargets: DEFAULT_TARGETS,
selectedQueryTargetsByType: DEFAULT_TARGETS_BY_TYPE,
setLastEditedQueryId: () => null,
setLastEditedQueryName: () => null,
setLastEditedQueryDescription: () => null,
@ -63,12 +71,14 @@ const initialState = {
setLastEditedQueryLoggingType: () => null,
setSelectedOsqueryTable: () => null,
setSelectedQueryTargets: () => null,
setSelectedQueryTargetsByType: () => null,
};
const actions = {
SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE",
SET_LAST_EDITED_QUERY_INFO: "SET_LAST_EDITED_QUERY_INFO",
SET_SELECTED_QUERY_TARGETS: "SET_SELECTED_QUERY_TARGETS",
SET_SELECTED_QUERY_TARGETS_BY_TYPE: "SET_SELECTED_QUERY_TARGETS_BY_TYPE",
} as const;
const reducer = (state: InitialStateType, action: any) => {
@ -128,6 +138,14 @@ const reducer = (state: InitialStateType, action: any) => {
? state.selectedQueryTargets
: action.selectedQueryTargets,
};
case actions.SET_SELECTED_QUERY_TARGETS_BY_TYPE:
return {
...state,
selectedQueryTargetsByType:
typeof action.selectedQueryTargetsByType === "undefined"
? state.selectedQueryTargetsByType
: action.selectedQueryTargetsByType,
};
default:
return state;
}
@ -150,6 +168,7 @@ const QueryProvider = ({ children }: Props) => {
lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion,
lastEditedQueryLoggingType: state.lastEditedQueryLoggingType,
selectedQueryTargets: state.selectedQueryTargets,
selectedQueryTargetsByType: state.selectedQueryTargetsByType,
setLastEditedQueryId: (lastEditedQueryId: number | null) => {
dispatch({
type: actions.SET_LAST_EDITED_QUERY_INFO,
@ -214,6 +233,14 @@ const QueryProvider = ({ children }: Props) => {
selectedQueryTargets,
});
},
setSelectedQueryTargetsByType: (
selectedQueryTargetsByType: ISelectedTargetsByType
) => {
dispatch({
type: actions.SET_SELECTED_QUERY_TARGETS_BY_TYPE,
selectedQueryTargetsByType,
});
},
setSelectedOsqueryTable: (tableName: string) => {
dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName });
},

View file

@ -4,7 +4,7 @@ import { filter, uniqueId } from "lodash";
import { IHost } from "interfaces/host";
import { ILabel } from "interfaces/label";
import { ITeam } from "interfaces/team";
import { ISelectedTargets } from "interfaces/target";
import { ISelectedTargetsForApi } from "interfaces/target";
import targetsAPI from "services/entities/targets";
export interface ITargetsLabels {
@ -25,7 +25,7 @@ export interface ITargetsQueryKey {
scope: string;
query: string;
queryId: number | null;
selected: ISelectedTargets;
selected: ISelectedTargetsForApi;
includeLabels: boolean;
}

View file

@ -38,12 +38,18 @@ export interface ISelectTeam extends ITeam {
export type ISelectTargetsEntity = ISelectHost | ISelectLabel | ISelectTeam;
export interface ISelectedTargets {
export interface ISelectedTargetsForApi {
hosts: number[];
labels: number[];
teams: number[];
}
export interface ISelectedTargetsByType {
hosts: IHost[];
labels: ILabel[];
teams: ITeam[];
}
export interface IPackTargets {
host_ids: (number | string)[];
label_ids: (number | string)[];
@ -52,3 +58,9 @@ export interface IPackTargets {
// TODO: Also use for testing
export const DEFAULT_TARGETS: ITarget[] = [];
export const DEFAULT_TARGETS_BY_TYPE: ISelectedTargetsByType = {
hosts: [],
labels: [],
teams: [],
};

View file

@ -13,6 +13,7 @@ import queryAPI from "services/entities/queries";
import teamAPI, { ILoadTeamsResponse } from "services/entities/teams";
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import { QueryContext } from "context/query";
import { NotificationContext } from "context/notification";
import {
IHost,
@ -26,6 +27,7 @@ import { ILabel } from "interfaces/label";
import { IHostPolicy } from "interfaces/policy";
import { IQueryStats } from "interfaces/query_stats";
import { ISoftware } from "interfaces/software";
import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target";
import { ITeam } from "interfaces/team";
import {
IListQueriesResponse,
@ -45,6 +47,7 @@ import {
TAGGED_TEMPLATES,
} from "utilities/helpers";
import permissions from "utilities/permissions";
import { DEFAULT_QUERY } from "utilities/constants";
import HostSummaryCard from "../cards/HostSummary";
import AboutCard from "../cards/About";
@ -133,6 +136,7 @@ const HostDetailsPage = ({
setLastEditedQueryCritical,
setPolicyTeamId,
} = useContext(PolicyContext);
const { setSelectedQueryTargetsByType } = useContext(QueryContext);
const { renderFlash } = useContext(NotificationContext);
const handlePageError = useErrorHandler();
@ -519,12 +523,15 @@ const HostDetailsPage = ({
};
const onQueryHostCustom = () => {
setLastEditedQueryBody(DEFAULT_QUERY.query);
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
router.push(
PATHS.NEW_QUERY() + TAGGED_TEMPLATES.queryByHostRoute(host?.id)
);
};
const onQueryHostSaved = (selectedQuery: ISchedulableQuery) => {
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
router.push(
PATHS.EDIT_QUERY(selectedQuery.id) +
TAGGED_TEMPLATES.queryByHostRoute(host?.id)

View file

@ -10,6 +10,7 @@ import { useQuery } from "react-query";
import { pick } from "lodash";
import { AppContext } from "context/app";
import { QueryContext } from "context/query";
import { TableContext } from "context/table";
import { NotificationContext } from "context/notification";
import { performanceIndicator } from "utilities/helpers";
@ -20,8 +21,10 @@ import {
IQueryKeyQueriesLoadAll,
ISchedulableQuery,
} from "interfaces/schedulable_query";
import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target";
import queriesAPI from "services/entities/queries";
import PATHS from "router/paths";
import { DEFAULT_QUERY } from "utilities/constants";
import { checkPlatformCompatibility } from "utilities/sql_tools";
import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
@ -87,6 +90,9 @@ const ManageQueriesPage = ({
isSandboxMode,
config,
} = useContext(AppContext);
const { setLastEditedQueryBody, setSelectedQueryTargetsByType } = useContext(
QueryContext
);
const { setResetSelectedRows } = useContext(TableContext);
const { renderFlash } = useContext(NotificationContext);
@ -178,7 +184,15 @@ const ManageQueriesPage = ({
}
}, [location, filteredQueriesPath, setFilteredQueriesPath]);
const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY(currentTeamId));
// Reset selected targets when returned to this page
useEffect(() => {
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
}, []);
const onCreateQueryClick = () => {
setLastEditedQueryBody(DEFAULT_QUERY.query);
router.push(PATHS.NEW_QUERY(currentTeamId));
};
const toggleDeleteQueryModal = useCallback(() => {
setShowDeleteQueryModal(!showDeleteQueryModal);

View file

@ -79,7 +79,6 @@ const EditQueryPage = ({
lastEditedQueryPlatforms,
lastEditedQueryLoggingType,
lastEditedQueryMinOsqueryVersion,
selectedQueryTargets,
setLastEditedQueryId,
setLastEditedQueryName,
setLastEditedQueryDescription,
@ -89,14 +88,10 @@ const EditQueryPage = ({
setLastEditedQueryLoggingType,
setLastEditedQueryMinOsqueryVersion,
setLastEditedQueryPlatforms,
// setSelectedQueryTargets,
} = useContext(QueryContext);
const { currentUser, setConfig } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
// const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
// const [targetedHosts, setTargetedHosts] = useState<IHost[]>([]);
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
@ -155,7 +150,7 @@ const EditQueryPage = ({
setLastEditedQueryId(DEFAULT_QUERY.id);
setLastEditedQueryName(DEFAULT_QUERY.name);
setLastEditedQueryDescription(DEFAULT_QUERY.description);
setLastEditedQueryBody(DEFAULT_QUERY.query);
// Persist lastEditedQueryBody through live query flow instead of resetting to DEFAULT_QUERY.query
setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run);
setLastEditedQueryFrequency(DEFAULT_QUERY.interval);
setLastEditedQueryLoggingType(DEFAULT_QUERY.logging);
@ -301,7 +296,10 @@ const EditQueryPage = ({
<div className={`${baseClass}_wrapper`}>
<div className={`${baseClass}__form`}>
<div className={`${baseClass}__header-links`}>
<BackLink text="Back to report" path={backToQueriesPath()} />
<BackLink
text={queryId ? "Back to report" : "Back to queries"}
path={backToQueriesPath()}
/>
</div>
<QueryForm
router={router}

View file

@ -594,11 +594,10 @@ const QueryForm = ({
className={`${baseClass}__run`}
variant="blue-green"
onClick={() => {
queryIdForEdit &&
router.push(
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
);
router.push(
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
);
}}
>
Live query
@ -800,11 +799,10 @@ const QueryForm = ({
className={`${baseClass}__run`}
variant="blue-green"
onClick={() => {
queryIdForEdit &&
router.push(
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
);
router.push(
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
);
}}
>
Live query

View file

@ -63,6 +63,8 @@ const RunQueryPage = ({
const {
selectedQueryTargets,
setSelectedQueryTargets,
selectedQueryTargetsByType,
setSelectedQueryTargetsByType,
setLastEditedQueryId,
setLastEditedQueryName,
setLastEditedQueryDescription,
@ -76,18 +78,18 @@ const RunQueryPage = ({
const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
const [step, setStep] = useState(LIVE_QUERY_STEPS[1]);
const [targetedHosts, setTargetedHosts] = useState<IHost[]>([]);
const [targetedLabels, setTargetedLabels] = useState<ILabel[]>([]);
const [targetedTeams, setTargetedTeams] = useState<ITeam[]>([]);
const [targetedHosts, setTargetedHosts] = useState<IHost[]>(
selectedQueryTargetsByType.hosts
);
const [targetedLabels, setTargetedLabels] = useState<ILabel[]>(
selectedQueryTargetsByType.labels
);
const [targetedTeams, setTargetedTeams] = useState<ITeam[]>(
selectedQueryTargetsByType.teams
);
const [targetsTotalCount, setTargetsTotalCount] = useState(0);
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
const TAGGED_TEMPLATES = {
queryByHostRoute: (hostId: number | undefined | null) => {
return `${hostId ? `?host_ids=${hostId}` : ""}`;
},
};
// disabled on page load so we can control the number of renders
// else it will re-populate the context on occasion
const { data: storedQuery } = useQuery<
@ -143,19 +145,21 @@ const RunQueryPage = ({
useEffect(() => {
detectIsFleetQueryRunnable();
if (!queryId) {
setLastEditedQueryId(DEFAULT_QUERY.id);
setLastEditedQueryName(DEFAULT_QUERY.name);
setLastEditedQueryDescription(DEFAULT_QUERY.description);
setLastEditedQueryBody(DEFAULT_QUERY.query);
setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run);
setLastEditedQueryFrequency(DEFAULT_QUERY.interval);
setLastEditedQueryLoggingType(DEFAULT_QUERY.logging);
setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version);
setLastEditedQueryPlatforms(DEFAULT_QUERY.platform);
}
}, [queryId]);
useEffect(() => {
setSelectedQueryTargetsByType({
hosts: targetedHosts,
labels: targetedLabels,
teams: targetedTeams,
});
}, [targetedLabels, targetedHosts, targetedTeams]);
console.log(
"LiveQueryPage.tsx: selectedQueryTargetsByType",
selectedQueryTargetsByType
);
// Updates title that shows up on browser tabs
useEffect(() => {
// e.g., Run live query | Discover TLS certificates | Fleet for osquery
@ -163,10 +167,12 @@ const RunQueryPage = ({
}, [location.pathname, storedQuery?.name]);
const goToQueryEditor = useCallback(
() => queryId && router.push(PATHS.EDIT_QUERY(queryId)),
() =>
queryId
? router.push(PATHS.EDIT_QUERY(queryId))
: router.push(PATHS.NEW_QUERY()),
[]
);
// const params = { id: paramsQueryId };
const renderScreen = () => {
const step1Props = {

View file

@ -222,12 +222,15 @@ const routes = (
<IndexRedirect to="manage" />
<Route path="manage" component={ManageQueriesPage} />
<Route component={AuthAnyMaintainerAdminObserverPlusRoutes}>
<Route path="new" component={EditQueryPage} />
<Route path="new">
<IndexRoute component={EditQueryPage} />
<Route path="live" component={LiveQueryPage} />
</Route>
</Route>
<Route path=":id">
<IndexRoute component={QueryDetailsPage} />
<Route path="edit" component={EditQueryPage} />
<Route path="run" component={LiveQueryPage} />
<Route path="live" component={LiveQueryPage} />
</Route>
</Route>
<Route path="policies">

View file

@ -57,8 +57,8 @@ export default {
teamId ? `?team_id=${teamId}` : ""
}`;
},
LIVE_QUERY: (queryId: number, teamId?: number): string => {
return `${URL_PREFIX}/queries/${queryId}/live${
LIVE_QUERY: (queryId: number | null, teamId?: number): string => {
return `${URL_PREFIX}/queries/${queryId || "new"}/live${
teamId ? `?team_id=${teamId}` : ""
}`;
},

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import sendRequest, { getError } from "services";
import endpoints from "utilities/endpoints";
import { ISelectedTargets } from "interfaces/target";
import { ISelectedTargetsForApi } from "interfaces/target";
import { AxiosResponse } from "axios";
import {
ICreateQueryRequestBody,
@ -52,7 +52,7 @@ export default {
}: {
query: string;
queryId: number | null;
selected: ISelectedTargets;
selected: ISelectedTargetsForApi;
}) => {
const { LIVE_QUERY } = endpoints;

View file

@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import sendRequest from "services";
import { IHost } from "interfaces/host";
import { ISelectedTargets, ITargetsAPIResponse } from "interfaces/target";
import { ISelectedTargetsForApi, ITargetsAPIResponse } from "interfaces/target";
import endpoints from "utilities/endpoints";
import appendTargetTypeToTargets from "utilities/append_target_type_to_targets";
interface ITargetsProps {
query?: string;
queryId?: number | null;
selected: ISelectedTargets;
selected: ISelectedTargetsForApi;
}
const defaultSelected = {
@ -29,7 +29,7 @@ export interface ITargetsSearchResponse {
export interface ITargetsCountParams {
query_id?: number | null;
selected: ISelectedTargets | null;
selected: ISelectedTargetsForApi | null;
}
export interface ITargetsCountResponse {

View file

@ -31,7 +31,7 @@ import {
} from "interfaces/scheduled_query";
import {
ISelectTargetsEntity,
ISelectedTargets,
ISelectedTargetsForApi,
IPackTargets,
} from "interfaces/target";
import { ITeam, ITeamSummary } from "interfaces/team";
@ -258,7 +258,7 @@ const formatLabelResponse = (response: any): ILabel[] => {
export const formatSelectedTargetsForApi = (
selectedTargets: ISelectTargetsEntity[]
): ISelectedTargets => {
): ISelectedTargetsForApi => {
const targets = selectedTargets || [];
// TODO: can flatMap be removed?
const hostIds = flatMap(targets, filterTarget("hosts"));