From c9e00a58f976e4e8f518c9441aae40a3d2f34b34 Mon Sep 17 00:00:00 2001 From: RachelElysia Date: Tue, 5 Nov 2024 17:12:03 -0500 Subject: [PATCH 1/2] Fleet UI: Add enable automations slider to query forms, small copy changes, add change log, add test --- changes/22224-query-log-destinations | 1 + .../components/forms/fields/Slider/Slider.tsx | 5 +- .../forms/fields/Slider/_styles.scss | 10 +- frontend/context/query.tsx | 15 +++ frontend/interfaces/schedulable_query.ts | 2 + frontend/pages/queries/edit/EditQueryPage.tsx | 5 + .../DiscardDataOption/DiscardDataOption.tsx | 9 +- .../EditQueryForm/EditQueryForm.tests.tsx | 91 ++++++++++++++++++- .../EditQueryForm/EditQueryForm.tsx | 44 +++++++++ .../SaveQueryModal/SaveQueryModal.tsx | 45 ++++++++- 10 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 changes/22224-query-log-destinations diff --git a/changes/22224-query-log-destinations b/changes/22224-query-log-destinations new file mode 100644 index 000000000000..b6172a331b0e --- /dev/null +++ b/changes/22224-query-log-destinations @@ -0,0 +1 @@ +- Creating a query allow users to turn on/off automations while being transparent of the current log destination diff --git a/frontend/components/forms/fields/Slider/Slider.tsx b/frontend/components/forms/fields/Slider/Slider.tsx index 2b368275f1d9..5b0a169a14d6 100644 --- a/frontend/components/forms/fields/Slider/Slider.tsx +++ b/frontend/components/forms/fields/Slider/Slider.tsx @@ -8,9 +8,10 @@ import { IFormFieldProps } from "components/forms/FormField/FormField"; interface ISliderProps { onChange: () => void; value: boolean; - inactiveText: string; - activeText: string; + inactiveText: JSX.Element | string; + activeText: JSX.Element | string; className?: string; + helpText?: JSX.Element | string; } const baseClass = "fleet-slider"; diff --git a/frontend/components/forms/fields/Slider/_styles.scss b/frontend/components/forms/fields/Slider/_styles.scss index 9b79918c29bd..8cd7c0a5c05b 100644 --- a/frontend/components/forms/fields/Slider/_styles.scss +++ b/frontend/components/forms/fields/Slider/_styles.scss @@ -24,7 +24,7 @@ &__wrapper { display: flex; align-items: center; - height: 40px; + height: 24px; // Noticeable with help text below slider } &__dot { @@ -40,10 +40,16 @@ } &__label { + display: flex; + vertical-align: middle; + gap: $pad-small; // For supporting icons font-size: $x-small; font-weight: $regular; text-align: left; - vertical-align: text-bottom; margin-left: $pad-small; } + + &__help-text { + @include help-text; + } } diff --git a/frontend/context/query.tsx b/frontend/context/query.tsx index f7d05f3b91d9..91c79538391e 100644 --- a/frontend/context/query.tsx +++ b/frontend/context/query.tsx @@ -25,6 +25,7 @@ type InitialStateType = { lastEditedQueryBody: string; lastEditedQueryObserverCanRun: boolean; lastEditedQueryFrequency: number; + lastEditedQueryAutomation: boolean; lastEditedQueryPlatforms: SelectedPlatformString; lastEditedQueryMinOsqueryVersion: string; lastEditedQueryLoggingType: QueryLoggingOption; @@ -38,6 +39,7 @@ type InitialStateType = { setLastEditedQueryBody: (value: string) => void; setLastEditedQueryObserverCanRun: (value: boolean) => void; setLastEditedQueryFrequency: (value: number) => void; + setLastEditedQueryAutomation: (value: boolean) => void; setLastEditedQueryPlatforms: (value: SelectedPlatformString) => void; setLastEditedQueryMinOsqueryVersion: (value: string) => void; setLastEditedQueryLoggingType: (value: string) => void; @@ -59,6 +61,7 @@ const initialState = { lastEditedQueryBody: DEFAULT_QUERY.query, lastEditedQueryObserverCanRun: DEFAULT_QUERY.observer_can_run, lastEditedQueryFrequency: DEFAULT_QUERY.interval, + lastEditedQueryAutomation: DEFAULT_QUERY.automations_enabled, lastEditedQueryPlatforms: DEFAULT_QUERY.platform, lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version, lastEditedQueryLoggingType: DEFAULT_QUERY.logging, @@ -72,6 +75,7 @@ const initialState = { setLastEditedQueryBody: () => null, setLastEditedQueryObserverCanRun: () => null, setLastEditedQueryFrequency: () => null, + setLastEditedQueryAutomation: () => null, setLastEditedQueryPlatforms: () => null, setLastEditedQueryMinOsqueryVersion: () => null, setLastEditedQueryLoggingType: () => null, @@ -125,6 +129,10 @@ const reducer = (state: InitialStateType, action: any) => { typeof action.lastEditedQueryFrequency === "undefined" ? state.lastEditedQueryFrequency : action.lastEditedQueryFrequency, + lastEditedQueryAutomation: + typeof action.lastEditedQueryAutomation === "undefined" + ? state.lastEditedQueryAutomation + : action.lastEditedQueryAutomation, lastEditedQueryPlatforms: typeof action.lastEditedQueryPlatforms === "undefined" ? state.lastEditedQueryPlatforms @@ -180,6 +188,7 @@ const QueryProvider = ({ children }: Props) => { lastEditedQueryBody: state.lastEditedQueryBody, lastEditedQueryObserverCanRun: state.lastEditedQueryObserverCanRun, lastEditedQueryFrequency: state.lastEditedQueryFrequency, + lastEditedQueryAutomation: state.lastEditedQueryAutomation, lastEditedQueryPlatforms: state.lastEditedQueryPlatforms, lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion, lastEditedQueryLoggingType: state.lastEditedQueryLoggingType, @@ -225,6 +234,12 @@ const QueryProvider = ({ children }: Props) => { lastEditedQueryFrequency, }); }, + setLastEditedQueryAutomation: (lastEditedQueryAutomation: boolean) => { + dispatch({ + type: actions.SET_LAST_EDITED_QUERY_INFO, + lastEditedQueryAutomation, + }); + }, setLastEditedQueryPlatforms: (lastEditedQueryPlatforms: string) => { dispatch({ type: actions.SET_LAST_EDITED_QUERY_INFO, diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index 23d2a9cd27d0..fb0fcac74bb5 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -100,6 +100,7 @@ export interface IModifyQueryRequestBody frequency?: number; platform?: SelectedPlatformString; min_osquery_version?: string; + automations_enabled?: boolean; } // response is ISchedulableQuery // better way to indicate this? @@ -131,6 +132,7 @@ export interface IEditQueryFormFields { observer_can_run: IFormField; discard_data: IFormField; frequency: IFormField; + automations_enabled: IFormField; platforms: IFormField; min_osquery_version: IFormField; logging: IFormField; diff --git a/frontend/pages/queries/edit/EditQueryPage.tsx b/frontend/pages/queries/edit/EditQueryPage.tsx index d2bc087b60c8..3a2f314c8d73 100644 --- a/frontend/pages/queries/edit/EditQueryPage.tsx +++ b/frontend/pages/queries/edit/EditQueryPage.tsx @@ -91,6 +91,7 @@ const EditQueryPage = ({ lastEditedQueryBody, lastEditedQueryObserverCanRun, lastEditedQueryFrequency, + lastEditedQueryAutomation, lastEditedQueryPlatforms, lastEditedQueryLoggingType, lastEditedQueryMinOsqueryVersion, @@ -101,6 +102,7 @@ const EditQueryPage = ({ setLastEditedQueryBody, setLastEditedQueryObserverCanRun, setLastEditedQueryFrequency, + setLastEditedQueryAutomation, setLastEditedQueryLoggingType, setLastEditedQueryMinOsqueryVersion, setLastEditedQueryPlatforms, @@ -150,6 +152,7 @@ const EditQueryPage = ({ setLastEditedQueryBody(returnedQuery.query); setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run); setLastEditedQueryFrequency(returnedQuery.interval); + setLastEditedQueryAutomation(returnedQuery.automations_enabled); setLastEditedQueryPlatforms(returnedQuery.platform); setLastEditedQueryLoggingType(returnedQuery.logging); setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version); @@ -228,6 +231,7 @@ const EditQueryPage = ({ // Persist lastEditedQueryBody through live query flow instead of resetting to DEFAULT_QUERY.query setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run); setLastEditedQueryFrequency(DEFAULT_QUERY.interval); + setLastEditedQueryAutomation(DEFAULT_QUERY.automations_enabled); setLastEditedQueryLoggingType(DEFAULT_QUERY.logging); setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version); setLastEditedQueryPlatforms(DEFAULT_QUERY.platform); @@ -298,6 +302,7 @@ const EditQueryPage = ({ lastEditedQueryBody, lastEditedQueryObserverCanRun, lastEditedQueryFrequency, + lastEditedQueryAutomation, lastEditedQueryPlatforms, lastEditedQueryLoggingType, lastEditedQueryMinOsqueryVersion, diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx index 4eee69abab81..6c65154862a4 100644 --- a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx +++ b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx @@ -60,14 +60,7 @@ const DiscardDataOption = ({ ) : ( - <> - The most recent results for each host will not be available in Fleet. - {breakHelpText ?
: " "} - Data will still be sent to your log destination if - automations - {" "} - are on. - + "The most recent results for each host will not be available in Fleet." )} ); diff --git a/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx index 81610b098fff..ab8a791fd39f 100644 --- a/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx +++ b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { screen } from "@testing-library/react"; +import { screen, within } from "@testing-library/react"; import { createCustomRenderer } from "test/test-utils"; import createMockQuery from "__mocks__/queryMock"; @@ -32,6 +32,7 @@ describe("EditQueryForm - component", () => { lastEditedQueryBody: mockQuery.query, lastEditedQueryObserverCanRun: mockQuery.observer_can_run, lastEditedQueryFrequency: mockQuery.interval, + lastEditedQueryAutomation: mockQuery.automations_enabled, lastEditedQueryPlatforms: mockQuery.platform, lastEditedQueryMinOsqueryVersion: mockQuery.min_osquery_version, lastEditedQueryLoggingType: mockQuery.logging, @@ -40,6 +41,7 @@ describe("EditQueryForm - component", () => { setLastEditedQueryBody: jest.fn(), setLastEditedQueryObserverCanRun: jest.fn(), setLastEditedQueryFrequency: jest.fn(), + setLastEditedQueryAutomation: jest.fn(), setLastEditedQueryPlatforms: jest.fn(), setLastEditedQueryMinOsqueryVersion: jest.fn(), setLastEditedQueryLoggingType: jest.fn(), @@ -92,6 +94,7 @@ describe("EditQueryForm - component", () => { lastEditedQueryBody: mockQuery.query, lastEditedQueryObserverCanRun: mockQuery.observer_can_run, lastEditedQueryFrequency: mockQuery.interval, + lastEditedQueryAutomation: mockQuery.automations_enabled, lastEditedQueryPlatforms: mockQuery.platform, lastEditedQueryMinOsqueryVersion: mockQuery.min_osquery_version, lastEditedQueryLoggingType: mockQuery.logging, @@ -100,6 +103,7 @@ describe("EditQueryForm - component", () => { setLastEditedQueryBody: jest.fn(), setLastEditedQueryObserverCanRun: jest.fn(), setLastEditedQueryFrequency: jest.fn(), + setLastEditedQueryAutomation: jest.fn(), setLastEditedQueryPlatforms: jest.fn(), setLastEditedQueryMinOsqueryVersion: jest.fn(), setLastEditedQueryLoggingType: jest.fn(), @@ -152,6 +156,91 @@ describe("EditQueryForm - component", () => { /live queries are disabled/i ); }); + + it("shows icon when query frequency is set to 0", async () => { + const render = createCustomRenderer({ + context: { + query: { + lastEditedQueryId: mockQuery.id, + lastEditedQueryName: "Test Query", + lastEditedQueryDescription: mockQuery.description, + lastEditedQueryBody: mockQuery.query, + lastEditedQueryObserverCanRun: mockQuery.observer_can_run, + lastEditedQueryFrequency: 0, // Set frequency to 0 + lastEditedQueryAutomation: true, // Enable automations + lastEditedQueryPlatforms: mockQuery.platform, + lastEditedQueryMinOsqueryVersion: mockQuery.min_osquery_version, + lastEditedQueryLoggingType: mockQuery.logging, + setLastEditedQueryName: jest.fn(), + setLastEditedQueryDescription: jest.fn(), + setLastEditedQueryBody: jest.fn(), + setLastEditedQueryObserverCanRun: jest.fn(), + setLastEditedQueryAutomation: jest.fn(), + setLastEditedQueryFrequency: jest.fn(), + setLastEditedQueryPlatforms: jest.fn(), + setLastEditedQueryMinOsqueryVersion: jest.fn(), + setLastEditedQueryLoggingType: jest.fn(), + }, + app: { + currentUser: createMockUser(), + isGlobalObserver: false, + isGlobalAdmin: true, + isGlobalMaintainer: false, + isOnGlobalTeam: true, + isPremiumTier: true, + isSandboxMode: false, + config: createMockConfig(), + }, + }, + }); + + const { user } = render( + + ); + + // Find the frequency dropdown + const frequencyDropdown = screen + .getByText("Frequency") + .closest(".form-field--dropdown") as HTMLElement; + expect(frequencyDropdown).toBeInTheDocument(); + + // Check if the frequency is set to "Never" + const selectedFrequency = within(frequencyDropdown).getByText("Never"); + expect(selectedFrequency).toBeInTheDocument(); + + // Find the automations slider + const automationsSlider = screen + .getByText("Automations on") + .closest(".fleet-slider__wrapper") as HTMLElement; + expect(automationsSlider).toBeInTheDocument(); + + // Check if the automations are enabled + const automationsButton = within(automationsSlider).getByRole("button"); + expect(automationsButton).toHaveClass("fleet-slider--active"); + + // Check if the warning icon is present + const warningIcon = within(automationsSlider).getByTestId("warning-icon"); + expect(warningIcon).toBeInTheDocument(); + }); + // TODO: Consider testing save button is disabled for a sql error // Trickiness is in modifying react-ace using react-testing library }); diff --git a/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx index 7a342cf3c7b5..ca646e97859a 100644 --- a/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx +++ b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx @@ -52,6 +52,8 @@ import RevealButton from "components/buttons/RevealButton"; import Checkbox from "components/forms/fields/Checkbox"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; +import Slider from "components/forms/fields/Slider"; +import TooltipWrapper from "components/TooltipWrapper"; import Spinner from "components/Spinner"; import Icon from "components/Icon/Icon"; import AutoSizeInputField from "components/forms/fields/AutoSizeInputField"; @@ -127,6 +129,7 @@ const EditQueryForm = ({ lastEditedQueryBody, lastEditedQueryObserverCanRun, lastEditedQueryFrequency, + lastEditedQueryAutomation, lastEditedQueryPlatforms, lastEditedQueryMinOsqueryVersion, lastEditedQueryLoggingType, @@ -136,6 +139,7 @@ const EditQueryForm = ({ setLastEditedQueryBody, setLastEditedQueryObserverCanRun, setLastEditedQueryFrequency, + setLastEditedQueryAutomation, setLastEditedQueryPlatforms, setLastEditedQueryMinOsqueryVersion, setLastEditedQueryLoggingType, @@ -176,6 +180,8 @@ const EditQueryForm = ({ const platformCompatibility = usePlatformCompatibility(); const { setCompatiblePlatforms } = platformCompatibility; + const logDestination = config?.logging.result.plugin || ""; + const debounceSQL = useDebouncedCallback((sql: string) => { const { errors: newErrors } = validateQuerySQL(sql); @@ -316,6 +322,7 @@ const EditQueryForm = ({ team_id: apiTeamIdForQuery, observer_can_run: lastEditedQueryObserverCanRun, interval: lastEditedQueryFrequency, + automations_enabled: lastEditedQueryAutomation, platform: lastEditedQueryPlatforms, min_osquery_version: lastEditedQueryMinOsqueryVersion, logging: lastEditedQueryLoggingType, @@ -341,6 +348,7 @@ const EditQueryForm = ({ team_id: apiTeamIdForQuery, observer_can_run: lastEditedQueryObserverCanRun, interval: lastEditedQueryFrequency, + automations_enabled: lastEditedQueryAutomation, platform: lastEditedQueryPlatforms, min_osquery_version: lastEditedQueryMinOsqueryVersion, logging: lastEditedQueryLoggingType, @@ -412,6 +420,7 @@ const EditQueryForm = ({ query: lastEditedQueryBody, observer_can_run: lastEditedQueryObserverCanRun, interval: lastEditedQueryFrequency, + automations_enabled: lastEditedQueryAutomation, platform: lastEditedQueryPlatforms, min_osquery_version: lastEditedQueryMinOsqueryVersion, logging: lastEditedQueryLoggingType, @@ -724,6 +733,41 @@ const EditQueryForm = ({ wrapperClassName={`${baseClass}__form-field form-field--frequency`} helpText="This is how often your query collects data." /> + + setLastEditedQueryAutomation(!lastEditedQueryAutomation) + } + value={lastEditedQueryAutomation} + activeText={ + <> + Automations on + {lastEditedQueryFrequency === 0 && ( + + Automations and reporting will be paused
+ for this query until a frequency is set. + + } + position="right" + tipOffset={9} + showArrow + underline={false} + > + +
+ )} + + } + inactiveText="Automations off" + helpText={ + <> + Historical results will + {!lastEditedQueryAutomation ? " not " : " "}be sent to your + log destination: {logDestination}. + + } + /> diff --git a/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx index 827d9b5bfe05..ab9b96fec64e 100644 --- a/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx +++ b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx @@ -1,6 +1,8 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useContext } from "react"; import { pull, size } from "lodash"; +import { AppContext } from "context/app"; + import useDeepEffect from "hooks/useDeepEffect"; import Checkbox from "components/forms/fields/Checkbox"; @@ -8,6 +10,9 @@ import Checkbox from "components/forms/fields/Checkbox"; import InputField from "components/forms/fields/InputField"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; +import Slider from "components/forms/fields/Slider"; +import TooltipWrapper from "components/TooltipWrapper"; +import Icon from "components/Icon"; import Button from "components/buttons/Button"; import Modal from "components/Modal"; import { @@ -58,6 +63,8 @@ const SaveQueryModal = ({ existingQuery, queryReportsDisabled, }: ISaveQueryModalProps): JSX.Element => { + const { config } = useContext(AppContext); + const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [selectedFrequency, setSelectedFrequency] = useState( @@ -76,12 +83,15 @@ const SaveQueryModal = ({ setSelectedLoggingType, ] = useState(existingQuery?.logging ?? "snapshot"); const [observerCanRun, setObserverCanRun] = useState(false); + const [automationsEnabled, setAutomationsEnabled] = useState(false); const [discardData, setDiscardData] = useState(false); const [errors, setErrors] = useState<{ [key: string]: string }>( backendValidators ); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + const logDestination = config?.logging.result.plugin || ""; + const toggleAdvancedOptions = () => { setShowAdvancedOptions(!showAdvancedOptions); }; @@ -115,6 +125,7 @@ const SaveQueryModal = ({ description, interval: selectedFrequency, observer_can_run: observerCanRun, + automations_enabled: automationsEnabled, discard_data: discardData, platform: selectedPlatformOptions, min_osquery_version: selectedMinOsqueryVersionOptions, @@ -197,6 +208,38 @@ const SaveQueryModal = ({ > Observers can run + setAutomationsEnabled(!automationsEnabled)} + value={automationsEnabled} + activeText={ + <> + Automations on + {selectedFrequency === 0 && ( + + Automations and reporting will be paused
+ for this query until a frequency is set. + + } + position="right" + tipOffset={9} + showArrow + underline={false} + > + +
+ )} + + } + inactiveText="Automations off" + helpText={ + <> + Historical results will {!automationsEnabled ? "not " : ""}be sent + to your log destination: {logDestination}. + + } + /> Date: Wed, 6 Nov 2024 10:44:02 -0500 Subject: [PATCH 2/2] Add and update tests --- .../DiscardDataOption.tests.tsx | 12 ++- .../DiscardDataOption/DiscardDataOption.tsx | 2 - .../EditQueryForm/EditQueryForm.tests.tsx | 2 +- .../SaveQueryModal/SaveQueryModal.tests.tsx | 88 +++++++++++++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tests.tsx diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx index 89cce972f124..c8acd957efc4 100644 --- a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx +++ b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx @@ -17,7 +17,11 @@ describe("DiscardDataOption component", () => { ); expect(screen.getByText(/Discard data/)).toBeInTheDocument(); - expect(screen.getByText(/Data will still be sent/)).toBeInTheDocument(); + expect( + screen.getByText( + /The most recent results for each host will not be available in Fleet./ + ) + ).toBeInTheDocument(); }); it('Renders the "disabled" help text with tooltip when the global option is disabled', async () => { @@ -48,7 +52,11 @@ describe("DiscardDataOption component", () => { await fireEvent.click(screen.getByText(/Edit anyway/)); // normal text - expect(screen.getByText(/Data will still be sent/)).toBeInTheDocument(); + expect( + screen.getByText( + /The most recent results for each host will not be available in Fleet./ + ) + ).toBeInTheDocument(); }); it('Renders the info banner when "Differential" logging option is selected', () => { render( diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx index 6c65154862a4..b7e3f04dc06e 100644 --- a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx +++ b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx @@ -13,7 +13,6 @@ interface IDiscardDataOptionProps { selectedLoggingType: QueryLoggingOption; discardData: boolean; setDiscardData: (value: boolean) => void; - breakHelpText?: boolean; } const DiscardDataOption = ({ @@ -21,7 +20,6 @@ const DiscardDataOption = ({ selectedLoggingType, discardData, setDiscardData, - breakHelpText = false, }: IDiscardDataOptionProps) => { const [forceEditDiscardData, setForceEditDiscardData] = useState(false); const disable = queryReportsDisabled && !forceEditDiscardData; diff --git a/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx index ab8a791fd39f..b79a504180eb 100644 --- a/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx +++ b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tests.tsx @@ -157,7 +157,7 @@ describe("EditQueryForm - component", () => { ); }); - it("shows icon when query frequency is set to 0", async () => { + it("shows automations warning icon when query frequency is set to 0", async () => { const render = createCustomRenderer({ context: { query: { diff --git a/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tests.tsx b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tests.tsx new file mode 100644 index 000000000000..6d5975928d71 --- /dev/null +++ b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tests.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import { screen, within } from "@testing-library/react"; +import { createCustomRenderer } from "test/test-utils"; +import createMockQuery from "__mocks__/queryMock"; +import createMockUser from "__mocks__/userMock"; +import createMockConfig from "__mocks__/configMock"; + +import SaveQueryModal from "./SaveQueryModal"; + +const mockQuery = createMockQuery(); + +describe("SaveQueryModal", () => { + const defaultProps = { + queryValue: "SELECT * FROM users", + apiTeamIdForQuery: 1, + isLoading: false, + saveQuery: jest.fn(), + toggleSaveQueryModal: jest.fn(), + backendValidators: {}, + existingQuery: mockQuery, + queryReportsDisabled: false, + }; + + it("renders the modal with initial values and allows editing", async () => { + const render = createCustomRenderer({ + context: { + app: { + currentUser: createMockUser(), + config: createMockConfig(), + }, + }, + }); + + const { user } = render(); + + expect(screen.getByLabelText("Name")).toBeInTheDocument(); + expect(screen.getByLabelText("Description")).toBeInTheDocument(); + expect(screen.getByText("Frequency")).toBeInTheDocument(); + expect(screen.getByText("Observers can run")).toBeInTheDocument(); + expect(screen.getByText("Automations off")).toBeInTheDocument(); + expect(screen.getByText("Show advanced options")).toBeInTheDocument(); + + const nameInput = screen.getByLabelText("Name"); + await user.type(nameInput, "Test Query"); + expect(nameInput).toHaveValue("Test Query"); + }); + + it("toggles advanced options", async () => { + const render = createCustomRenderer({ + context: { + app: { + currentUser: createMockUser(), + config: createMockConfig(), + }, + }, + }); + + const { user } = render(); + + const advancedOptionsButton = screen.getByText("Show advanced options"); + await user.click(advancedOptionsButton); + + expect(screen.getByText("Platforms")).toBeInTheDocument(); + expect(screen.getByText("Minimum osquery version")).toBeInTheDocument(); + expect(screen.getByText("Logging")).toBeInTheDocument(); + + await user.click(advancedOptionsButton); + + expect(screen.queryByText("Platforms")).not.toBeInTheDocument(); + }); + + it("displays error when query name is empty", async () => { + const render = createCustomRenderer({ + context: { + app: { + currentUser: createMockUser(), + config: createMockConfig(), + }, + }, + }); + + const { user } = render(); + + await user.click(screen.getByText("Save")); + + expect(screen.getByText("Query name must be present")).toBeInTheDocument(); + }); +});