Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI – Componentize "Discard data" option and add it to Edit Query page #14343

Merged
merged 9 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions frontend/context/query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type InitialStateType = {
lastEditedQueryPlatforms: SelectedPlatformString;
lastEditedQueryMinOsqueryVersion: string;
lastEditedQueryLoggingType: QueryLoggingOption;
lastEditedQueryDiscardData: boolean;
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;
Expand All @@ -39,6 +40,7 @@ type InitialStateType = {
setLastEditedQueryPlatforms: (value: SelectedPlatformString) => void;
setLastEditedQueryMinOsqueryVersion: (value: string) => void;
setLastEditedQueryLoggingType: (value: string) => void;
setLastEditedQueryDiscardData: (value: boolean) => void;
setSelectedOsqueryTable: (tableName: string) => void;
setSelectedQueryTargets: (value: ITarget[]) => void;
setSelectedQueryTargetsByType: (value: ISelectedTargetsByType) => void;
Expand All @@ -58,6 +60,7 @@ const initialState = {
lastEditedQueryPlatforms: DEFAULT_QUERY.platform,
lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version,
lastEditedQueryLoggingType: DEFAULT_QUERY.logging,
lastEditedQueryDiscardData: DEFAULT_QUERY.discard_data,
selectedQueryTargets: DEFAULT_TARGETS,
selectedQueryTargetsByType: DEFAULT_TARGETS_BY_TYPE,
setLastEditedQueryId: () => null,
Expand All @@ -69,6 +72,7 @@ const initialState = {
setLastEditedQueryPlatforms: () => null,
setLastEditedQueryMinOsqueryVersion: () => null,
setLastEditedQueryLoggingType: () => null,
setLastEditedQueryDiscardData: () => null,
setSelectedOsqueryTable: () => null,
setSelectedQueryTargets: () => null,
setSelectedQueryTargetsByType: () => null,
Expand Down Expand Up @@ -129,6 +133,10 @@ const reducer = (state: InitialStateType, action: any) => {
typeof action.lastEditedQueryLoggingType === "undefined"
? state.lastEditedQueryLoggingType
: action.lastEditedQueryLoggingType,
lastEditedQueryDiscardData:
typeof action.lastEditedQueryDiscardData === "undefined"
? state.lastEditedQueryDiscardData
: action.lastEditedQueryDiscardData,
};
case actions.SET_SELECTED_QUERY_TARGETS:
return {
Expand Down Expand Up @@ -167,6 +175,7 @@ const QueryProvider = ({ children }: Props) => {
lastEditedQueryPlatforms: state.lastEditedQueryPlatforms,
lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion,
lastEditedQueryLoggingType: state.lastEditedQueryLoggingType,
lastEditedQueryDiscardData: state.lastEditedQueryDiscardData,
selectedQueryTargets: state.selectedQueryTargets,
selectedQueryTargetsByType: state.selectedQueryTargetsByType,
setLastEditedQueryId: (lastEditedQueryId: number | null) => {
Expand Down Expand Up @@ -227,6 +236,12 @@ const QueryProvider = ({ children }: Props) => {
lastEditedQueryLoggingType,
});
},
setLastEditedQueryDiscardData: (lastEditedQueryDiscardData: boolean) => {
dispatch({
type: actions.SET_LAST_EDITED_QUERY_INFO,
lastEditedQueryDiscardData,
});
},
setSelectedQueryTargets: (selectedQueryTargets: ITarget[]) => {
dispatch({
type: actions.SET_SELECTED_QUERY_TARGETS,
Expand Down
4 changes: 2 additions & 2 deletions frontend/interfaces/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IPack } from "./pack";
import { ISchedulableQuery } from "./schedulable_query";
import { IScheduledQueryStats } from "./scheduled_query_stats";

export interface IQueryFormData {
export interface IEditQueryFormData {
description?: string | number | boolean | undefined;
name?: string | number | boolean | undefined;
query?: string | number | boolean | undefined;
Expand Down Expand Up @@ -35,7 +35,7 @@ export interface IQuery {
stats?: IScheduledQueryStats;
}

export interface IQueryFormFields {
export interface IEditQueryFormFields {
description: IFormField;
name: IFormField;
query: IFormField;
Expand Down
2 changes: 1 addition & 1 deletion frontend/interfaces/schedulable_query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export interface IDeleteQueriesResponse {
deleted: number; // number of queries deleted
}

export interface IQueryFormFields {
export interface IEditQueryFormFields {
name: IFormField<string>;
description: IFormField<string>;
query: IFormField<string>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,7 @@ const generateTableHeaders = ({
<TextCell
value={val}
emptyCellTooltipText={
<>
Assign a frequency and turn <strong>automations</strong> on to
collect data at an interval.
</>
<>Assign a frequency to collect data at an interval.</>
}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import debounce from "utilities/debounce";
import deepDifference from "utilities/deep_difference";

import BackLink from "components/BackLink";
import QueryForm from "pages/queries/edit/components/QueryForm";
import EditQueryForm from "pages/queries/edit/components/EditQueryForm";
import { IConfig } from "interfaces/config";

interface IEditQueryPageProps {
Expand Down Expand Up @@ -89,26 +89,29 @@ const EditQueryPage = ({
setLastEditedQueryMinOsqueryVersion,
setLastEditedQueryPlatforms,
} = useContext(QueryContext);
const { currentUser, setConfig } = useContext(AppContext);
const { setConfig } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);

const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
false
);
const [showSaveChangesModal, setShowSaveChangesModal] = useState(false);

const { data: appConfig, isLoading: isLoadingAppConfig } = useQuery<
IConfig,
Error,
IConfig
>(["config"], () => configAPI.loadAll(), {
select: (data: IConfig) => data,
onSuccess: (data) => {
setConfig(data);
},
});
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
Expand Down Expand Up @@ -238,7 +241,7 @@ const EditQueryPage = ({
}

setIsQueryUpdating(false);
setShowSaveChangesModal(false); // Closes conditionally opened modal when discarding previous results
setShowConfirmSaveChangesModal(false); // Closes conditionally opened modal when discarding previous results

return false;
};
Expand Down Expand Up @@ -301,7 +304,7 @@ const EditQueryPage = ({
path={backToQueriesPath()}
/>
</div>
<QueryForm
<EditQueryForm
router={router}
saveQuery={saveQuery}
onOsqueryTableSelect={onOsqueryTableSelect}
Expand All @@ -318,10 +321,11 @@ const EditQueryPage = ({
isQuerySaving={isQuerySaving}
isQueryUpdating={isQueryUpdating}
hostId={parseInt(location.query.host_ids as string, 10)}
appConfig={appConfig}
isLoadingAppConfig={isLoadingAppConfig}
showSaveChangesModal={showSaveChangesModal}
setShowSaveChangesModal={setShowSaveChangesModal}
queryReportsDisabled={
appConfig?.server_settings.query_reports_disabled
}
showConfirmSaveChangesModal={showConfirmSaveChangesModal}
setShowConfirmSaveChangesModal={setShowConfirmSaveChangesModal}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
.edit-query-page {
.help-text {
margin-top: $pad-small;
margin-bottom: $pad-large;
font-weight: $regular;
font-size: $xx-small;
color: $ui-fleet-black-75;
}

.fleet-checkbox {
display: flex;
align-items: center;
}

.form-field {
&--frequency {
margin-bottom: 0;
}
&--platform {
margin-bottom: 0;
margin-top: $pad-large;
}
}

.advanced-options-toggle {
font-weight: $xbold;
}

.observer-can-run-wrapper {
margin-bottom: 0;
font-weight: bold;
}

.body-wrap {
min-width: 0;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";

import Button from "components/buttons/Button";
import Modal from "components/Modal";

const baseClass = "save-changes-modal";

export interface IConfirmSaveChangesModalProps {
isUpdating: boolean;
onSaveChanges: (evt: React.MouseEvent<HTMLButtonElement>) => void;
onClose: () => void;
showChangedSQLCopy?: boolean;
}

const ConfirmSaveChangesModal = ({
isUpdating,
onSaveChanges,
onClose,
showChangedSQLCopy = false,
}: IConfirmSaveChangesModalProps) => {
const warningText = showChangedSQLCopy
? "Changing this query's SQL will delete its previous results, since the existing report does not reflect the updated query."
: "The changes you are making to this query will delete its previous results.";

return (
<Modal title={"Save changes?"} onExit={onClose}>
<form className={`${baseClass}__form`}>
<p>{warningText}</p>
<p>You cannot undo this action.</p>
<div className="modal-cta-wrap">
<Button
type="button"
onClick={onSaveChanges}
variant="brand"
className="save-loading"
isLoading={isUpdating}
>
Save
</Button>
<Button onClick={onClose} variant="inverse">
Cancel
</Button>
</div>
</form>
</Modal>
);
};

export default ConfirmSaveChangesModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./ConfirmSaveChangesModal";
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Meta, StoryObj } from "@storybook/react";

import DiscardDataOption from "./DiscardDataOption";

const meta: Meta<typeof DiscardDataOption> = {
title: "Components/DiscardDataOption",
component: DiscardDataOption,
};

export default meta;

type Story = StoryObj<typeof DiscardDataOption>;

export const Basic: Story = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from "react";

import { fireEvent, render, screen } from "@testing-library/react";

import DiscardDataOption from "./DiscardDataOption";

describe("DiscardDataOption component", () => {
const selectedLoggingType = "snapshot";
const [discardData, setDiscardData] = [false, jest.fn()];

it("Renders normal help text when the global option is not disabled", () => {
render(
<DiscardDataOption
queryReportsDisabled={false}
{...{ selectedLoggingType, discardData, setDiscardData }}
/>
);

expect(screen.getByText(/Discard data/)).toBeInTheDocument();
expect(screen.getByText(/Data will still be sent/)).toBeInTheDocument();
});

it('Renders the "disabled" help text with tooltip when the global option is disabled', async () => {
render(
<DiscardDataOption
queryReportsDisabled
{...{ selectedLoggingType, discardData, setDiscardData }}
/>
);

expect(screen.getByText(/Discard data/)).toBeInTheDocument();
expect(screen.getByText(/This setting is ignored/)).toBeInTheDocument();

await fireEvent.mouseOver(screen.getByText(/globally disabled/));

expect(screen.getByText(/A Fleet administrator/)).toBeInTheDocument();
});

it('Restores normal help text when disabled and then "Edit anyway" is clicked', async () => {
render(
<DiscardDataOption
queryReportsDisabled
{...{ selectedLoggingType, discardData, setDiscardData }}
/>
);

// disabled
expect(screen.getByText(/Discard data/)).toBeInTheDocument();
expect(screen.getByText(/This setting is ignored/)).toBeInTheDocument();

// enable
await fireEvent.click(screen.getByText(/Edit anyway/));

// normal text
expect(screen.getByText(/Data will still be sent/)).toBeInTheDocument();
});
it('Renders the info banner when "Differential" logging option is selected', () => {
render(
<DiscardDataOption
selectedLoggingType="differential"
queryReportsDisabled={false}
{...{ discardData, setDiscardData }}
/>
);

expect(
screen.getByText(
/setting is ignored when differential logging is enabled. This/
)
).toBeInTheDocument();
});
it('Renders the info banner when "Differential (ignore removals)" logging option is selected', () => {
render(
<DiscardDataOption
selectedLoggingType="differential_ignore_removals"
queryReportsDisabled={false}
{...{ discardData, setDiscardData }}
/>
);
expect(
screen.getByText(
/setting is ignored when differential logging is enabled. This/
)
).toBeInTheDocument();
});
});
Loading