diff --git a/frontend/components/LiveQuery/SelectTargets.tsx b/frontend/components/LiveQuery/SelectTargets.tsx index 2052d13e14be..998e4e6d523d 100644 --- a/frontend/components/LiveQuery/SelectTargets.tsx +++ b/frontend/components/LiveQuery/SelectTargets.tsx @@ -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> // Used for policies page level useState hook + setSelectedTargets: // TODO: Refactor policy targets to streamline selectedTargets/selectedTargetsByType + | React.Dispatch> // Used for policies page level useState hook | ((value: ITarget[]) => void); // Used for queries app level QueryContext setTargetedHosts: React.Dispatch>; setTargetedLabels: React.Dispatch>; @@ -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 ( <> {total} host{total > 1 ? `s` : ``} targeted  ( - {onlinePercentage} + {onlinePercentage()} %  have recently checked
into Fleet.`} diff --git a/frontend/context/query.tsx b/frontend/context/query.tsx index 4359eee70f49..73bd7a297e69 100644 --- a/frontend/context/query.tsx +++ b/frontend/context/query.tsx @@ -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; @@ -24,7 +29,8 @@ type InitialStateType = { lastEditedQueryMinOsqueryVersion: string; lastEditedQueryLoggingType: QueryLoggingOption; lastEditedQueryDiscardData: boolean; - 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; @@ -37,6 +43,7 @@ type InitialStateType = { setLastEditedQueryDiscardData: (value: boolean) => void; setSelectedOsqueryTable: (tableName: string) => void; setSelectedQueryTargets: (value: ITarget[]) => void; + setSelectedQueryTargetsByType: (value: ISelectedTargetsByType) => void; }; export type IQueryContext = InitialStateType; @@ -55,6 +62,7 @@ const initialState = { lastEditedQueryLoggingType: DEFAULT_QUERY.logging, lastEditedQueryDiscardData: DEFAULT_QUERY.discard_data, selectedQueryTargets: DEFAULT_TARGETS, + selectedQueryTargetsByType: DEFAULT_TARGETS_BY_TYPE, setLastEditedQueryId: () => null, setLastEditedQueryName: () => null, setLastEditedQueryDescription: () => null, @@ -67,12 +75,14 @@ const initialState = { setLastEditedQueryDiscardData: () => 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) => { @@ -136,6 +146,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; } @@ -159,6 +177,7 @@ const QueryProvider = ({ children }: Props) => { lastEditedQueryLoggingType: state.lastEditedQueryLoggingType, lastEditedQueryDiscardData: state.lastEditedQueryDiscardData, selectedQueryTargets: state.selectedQueryTargets, + selectedQueryTargetsByType: state.selectedQueryTargetsByType, setLastEditedQueryId: (lastEditedQueryId: number | null) => { dispatch({ type: actions.SET_LAST_EDITED_QUERY_INFO, @@ -229,6 +248,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 }); }, diff --git a/frontend/hooks/useQueryTargets.ts b/frontend/hooks/useQueryTargets.ts index 081b473afe4f..d594ab856670 100644 --- a/frontend/hooks/useQueryTargets.ts +++ b/frontend/hooks/useQueryTargets.ts @@ -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; } diff --git a/frontend/interfaces/target.ts b/frontend/interfaces/target.ts index 873d7907f36a..06f464cf51b3 100644 --- a/frontend/interfaces/target.ts +++ b/frontend/interfaces/target.ts @@ -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: [], +}; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 132dd5b502be..0493579f5e11 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -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) diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index b83ea47c99dc..73290cecab6b 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -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); diff --git a/frontend/pages/queries/edit/EditQueryPage.tsx b/frontend/pages/queries/edit/EditQueryPage.tsx index 32b99c73c735..f0d164a8a5e3 100644 --- a/frontend/pages/queries/edit/EditQueryPage.tsx +++ b/frontend/pages/queries/edit/EditQueryPage.tsx @@ -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 { setConfig } = useContext(AppContext); const { renderFlash } = useContext(NotificationContext); - // const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false); - // const [targetedHosts, setTargetedHosts] = useState([]); - const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState( @@ -158,7 +153,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); @@ -304,7 +299,10 @@ const EditQueryPage = ({
- +
{ - queryIdForEdit && - router.push( - PATHS.LIVE_QUERY(queryIdForEdit) + - TAGGED_TEMPLATES.queryByHostRoute(hostId) - ); + router.push( + PATHS.LIVE_QUERY(queryIdForEdit) + + TAGGED_TEMPLATES.queryByHostRoute(hostId) + ); }} > Live query @@ -819,11 +818,10 @@ const EditQueryForm = ({ 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 diff --git a/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx index c49d4c333bd3..127f3b17c227 100644 --- a/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx +++ b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx @@ -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([]); - const [targetedLabels, setTargetedLabels] = useState([]); - const [targetedTeams, setTargetedTeams] = useState([]); + const [targetedHosts, setTargetedHosts] = useState( + selectedQueryTargetsByType.hosts + ); + const [targetedLabels, setTargetedLabels] = useState( + selectedQueryTargetsByType.labels + ); + const [targetedTeams, setTargetedTeams] = useState( + 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 = { diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index 07f125643671..5410c1ac99fb 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -222,12 +222,15 @@ const routes = ( - + + + + - + diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index 3190f09c45d9..ce1b2b582f62 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -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}` : "" }`; }, diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index bfad2ff9ed7f..355429d3169e 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -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; diff --git a/frontend/services/entities/targets.ts b/frontend/services/entities/targets.ts index 0e00bc286fbe..400733da22a7 100644 --- a/frontend/services/entities/targets.ts +++ b/frontend/services/entities/targets.ts @@ -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 { diff --git a/frontend/utilities/helpers.ts b/frontend/utilities/helpers.ts index c519b9a26313..89b291524b0d 100644 --- a/frontend/utilities/helpers.ts +++ b/frontend/utilities/helpers.ts @@ -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"));