Fleet UI: Add enable automations slider to query forms (#23562)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Creating a query allow users to turn on/off automations while being transparent of the current log destination
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ type InitialStateType = {
|
||||
lastEditedQueryBody: string;
|
||||
lastEditedQueryObserverCanRun: boolean;
|
||||
lastEditedQueryFrequency: number;
|
||||
lastEditedQueryAutomationsEnabled: 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;
|
||||
setLastEditedQueryAutomationsEnabled: (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,
|
||||
lastEditedQueryAutomationsEnabled: 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,
|
||||
setLastEditedQueryAutomationsEnabled: () => 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,
|
||||
lastEditedQueryAutomationsEnabled:
|
||||
typeof action.lastEditedQueryAutomationsEnabled === "undefined"
|
||||
? state.lastEditedQueryAutomationsEnabled
|
||||
: action.lastEditedQueryAutomationsEnabled,
|
||||
lastEditedQueryPlatforms:
|
||||
typeof action.lastEditedQueryPlatforms === "undefined"
|
||||
? state.lastEditedQueryPlatforms
|
||||
@@ -180,6 +188,7 @@ const QueryProvider = ({ children }: Props) => {
|
||||
lastEditedQueryBody: state.lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun: state.lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency: state.lastEditedQueryFrequency,
|
||||
lastEditedQueryAutomationsEnabled: state.lastEditedQueryAutomationsEnabled,
|
||||
lastEditedQueryPlatforms: state.lastEditedQueryPlatforms,
|
||||
lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion,
|
||||
lastEditedQueryLoggingType: state.lastEditedQueryLoggingType,
|
||||
@@ -225,6 +234,14 @@ const QueryProvider = ({ children }: Props) => {
|
||||
lastEditedQueryFrequency,
|
||||
});
|
||||
},
|
||||
setLastEditedQueryAutomationsEnabled: (
|
||||
lastEditedQueryAutomationsEnabled: boolean
|
||||
) => {
|
||||
dispatch({
|
||||
type: actions.SET_LAST_EDITED_QUERY_INFO,
|
||||
lastEditedQueryAutomationsEnabled,
|
||||
});
|
||||
},
|
||||
setLastEditedQueryPlatforms: (lastEditedQueryPlatforms: string) => {
|
||||
dispatch({
|
||||
type: actions.SET_LAST_EDITED_QUERY_INFO,
|
||||
|
||||
@@ -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<boolean>;
|
||||
discard_data: IFormField<boolean>;
|
||||
frequency: IFormField<number>;
|
||||
automations_enabled: IFormField<boolean>;
|
||||
platforms: IFormField<SelectedPlatformString>;
|
||||
min_osquery_version: IFormField<string>;
|
||||
logging: IFormField<QueryLoggingOption>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ const EditQueryPage = ({
|
||||
lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency,
|
||||
lastEditedQueryAutomationsEnabled,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryLoggingType,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
@@ -101,6 +102,7 @@ const EditQueryPage = ({
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryObserverCanRun,
|
||||
setLastEditedQueryFrequency,
|
||||
setLastEditedQueryAutomationsEnabled,
|
||||
setLastEditedQueryLoggingType,
|
||||
setLastEditedQueryMinOsqueryVersion,
|
||||
setLastEditedQueryPlatforms,
|
||||
@@ -150,6 +152,7 @@ const EditQueryPage = ({
|
||||
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);
|
||||
@@ -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);
|
||||
setLastEditedQueryAutomationsEnabled(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,
|
||||
lastEditedQueryAutomationsEnabled,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryLoggingType,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
|
||||
+10
-2
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
@@ -60,14 +58,7 @@ const DiscardDataOption = ({
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
The most recent results for each host will not be available in Fleet.
|
||||
{breakHelpText ? <br /> : " "}
|
||||
Data will still be sent to your log destination if <b>
|
||||
automations
|
||||
</b>{" "}
|
||||
are <b>on</b>.
|
||||
</>
|
||||
"The most recent results for each host will not be available in Fleet."
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
lastEditedQueryAutomationsEnabled: 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(),
|
||||
setLastEditedQueryAutomationsEnabled: 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,
|
||||
lastEditedQueryAutomationsEnabled: 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(),
|
||||
setLastEditedQueryAutomationsEnabled: 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 automations warning 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
|
||||
lastEditedQueryAutomationsEnabled: 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(),
|
||||
setLastEditedQueryAutomationsEnabled: 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(
|
||||
<EditQueryForm
|
||||
router={mockRouter}
|
||||
queryIdForEdit={1}
|
||||
apiTeamIdForQuery={1}
|
||||
teamNameForQuery="Apples"
|
||||
showOpenSchemaActionText
|
||||
storedQuery={createMockQuery({ interval: 0 })}
|
||||
isStoredQueryLoading={false}
|
||||
isQuerySaving={false}
|
||||
isQueryUpdating={false}
|
||||
onSubmitNewQuery={jest.fn()}
|
||||
onOsqueryTableSelect={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
showConfirmSaveChangesModal={false}
|
||||
setShowConfirmSaveChangesModal={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
lastEditedQueryAutomationsEnabled,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
lastEditedQueryLoggingType,
|
||||
@@ -136,6 +139,7 @@ const EditQueryForm = ({
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryObserverCanRun,
|
||||
setLastEditedQueryFrequency,
|
||||
setLastEditedQueryAutomationsEnabled,
|
||||
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: lastEditedQueryAutomationsEnabled,
|
||||
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: lastEditedQueryAutomationsEnabled,
|
||||
platform: lastEditedQueryPlatforms,
|
||||
min_osquery_version: lastEditedQueryMinOsqueryVersion,
|
||||
logging: lastEditedQueryLoggingType,
|
||||
@@ -412,6 +420,7 @@ const EditQueryForm = ({
|
||||
query: lastEditedQueryBody,
|
||||
observer_can_run: lastEditedQueryObserverCanRun,
|
||||
interval: lastEditedQueryFrequency,
|
||||
automations_enabled: lastEditedQueryAutomationsEnabled,
|
||||
platform: lastEditedQueryPlatforms,
|
||||
min_osquery_version: lastEditedQueryMinOsqueryVersion,
|
||||
logging: lastEditedQueryLoggingType,
|
||||
@@ -724,6 +733,43 @@ const EditQueryForm = ({
|
||||
wrapperClassName={`${baseClass}__form-field form-field--frequency`}
|
||||
helpText="This is how often your query collects data."
|
||||
/>
|
||||
<Slider
|
||||
onChange={() =>
|
||||
setLastEditedQueryAutomationsEnabled(
|
||||
!lastEditedQueryAutomationsEnabled
|
||||
)
|
||||
}
|
||||
value={lastEditedQueryAutomationsEnabled}
|
||||
activeText={
|
||||
<>
|
||||
Automations on
|
||||
{lastEditedQueryFrequency === 0 && (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<>
|
||||
Automations and reporting will be paused <br />
|
||||
for this query until a frequency is set.
|
||||
</>
|
||||
}
|
||||
position="right"
|
||||
tipOffset={9}
|
||||
showArrow
|
||||
underline={false}
|
||||
>
|
||||
<Icon name="warning" />
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
inactiveText="Automations off"
|
||||
helpText={
|
||||
<>
|
||||
Historical results will
|
||||
{!lastEditedQueryAutomationsEnabled ? " not " : " "}be sent
|
||||
to your log destination: <b>{logDestination}</b>.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
value={lastEditedQueryObserverCanRun}
|
||||
onChange={(value: boolean) =>
|
||||
|
||||
@@ -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(<SaveQueryModal {...defaultProps} />);
|
||||
|
||||
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(<SaveQueryModal {...defaultProps} />);
|
||||
|
||||
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(<SaveQueryModal {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByText("Save"));
|
||||
|
||||
expect(screen.getByText("Query name must be present")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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<QueryLoggingOption>(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
|
||||
</Checkbox>
|
||||
<Slider
|
||||
onChange={() => setAutomationsEnabled(!automationsEnabled)}
|
||||
value={automationsEnabled}
|
||||
activeText={
|
||||
<>
|
||||
Automations on
|
||||
{selectedFrequency === 0 && (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<>
|
||||
Automations and reporting will be paused <br />
|
||||
for this query until a frequency is set.
|
||||
</>
|
||||
}
|
||||
position="right"
|
||||
tipOffset={9}
|
||||
showArrow
|
||||
underline={false}
|
||||
>
|
||||
<Icon name="warning" />
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
inactiveText="Automations off"
|
||||
helpText={
|
||||
<>
|
||||
Historical results will {!automationsEnabled ? "not " : ""}be sent
|
||||
to your log destination: <b>{logDestination}</b>.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
className="advanced-options-toggle"
|
||||
|
||||
Reference in New Issue
Block a user