diff --git a/changes/12645-manage-query-automations b/changes/12645-manage-query-automations new file mode 100644 index 0000000000..0df3de75f3 --- /dev/null +++ b/changes/12645-manage-query-automations @@ -0,0 +1 @@ +- Users able to manage schedulable queries (new feature) with automations modal diff --git a/frontend/__mocks__/scheduleableQueryMock.ts b/frontend/__mocks__/scheduleableQueryMock.ts index e437edc121..edc82a61da 100644 --- a/frontend/__mocks__/scheduleableQueryMock.ts +++ b/frontend/__mocks__/scheduleableQueryMock.ts @@ -10,7 +10,7 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { description: "A test query", query: "SELECT * FROM users", team_id: null, - interval: 3600, + interval: 43200, // Every 12 hours platform: "darwin,windows,linux", min_osquery_version: "", automations_enabled: true, @@ -22,11 +22,11 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = { observer_can_run: false, packs: [], stats: { - system_time_p50: 1, - system_time_p95: 1, - user_time_p50: 1, - user_time_p95: 1, - total_executions: 3, + system_time_p50: 28.1053, + system_time_p95: 397.6667, + user_time_p50: 29.9412, + user_time_p95: 251.4615, + total_executions: 5746, }, }; diff --git a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx new file mode 100644 index 0000000000..a39903f806 --- /dev/null +++ b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.stories.tsx @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import LogDestinationIndicator from "./LogDestinationIndicator"; + +const meta: Meta = { + title: "Components/LogDestinationIndicator", + component: LogDestinationIndicator, + args: { + logDestination: "filesystem", + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx new file mode 100644 index 0000000000..82de7b5d0b --- /dev/null +++ b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import classnames from "classnames"; +import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper"; +import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; + +interface ILogDestinationIndicatorProps { + logDestination: string; +} + +const generateClassTag = (rawValue: string): string => { + if (rawValue === DEFAULT_EMPTY_CELL_VALUE) { + return "indeterminate"; + } + return rawValue.replace(" ", "-").toLowerCase(); +}; + +const LogDestinationIndicator = ({ + logDestination, +}: ILogDestinationIndicatorProps): JSX.Element => { + const classTag = generateClassTag(logDestination); + const statusClassName = classnames( + "log-destination-indicator", + `log-destination-indicator--${classTag}`, + `log-destination--${classTag}` + ); + const readableLogDestination = () => { + switch (logDestination) { + case "filesystem": + return "Filesystem"; + case "firehose": + return "Amazon Kinesis Data Firehose"; + case "kinesis": + return "Amazon Kinesis Data Streams"; + case "lambda": + return "AWS Lambda"; + case "pubsub": + return "Google Cloud Pub/Sub"; + case "kafta": + return "Apache Kafka"; + case "stdout": + return "Standard output (stdout)"; + case "": + return "Not configured"; + default: + return logDestination; + } + }; + + const tooltipText = () => { + switch (logDestination) { + case "filesystem": + return `Each time a query runs, the data is sent to
+ /var/log/osquery/osqueryd.snapshots.log
+ in each host's filesystem.`; + case "firehose": + return `Each time a query runs, the data is sent to
+ Amazon Kinesis Data Firehose.`; + case "kinesis": + return `Each time a query runs, the data is sent to
+ Amazon Kinesis Data Streams.`; + case "lambda": + return ` + Each time a query runs, the data
is sent to AWS Lambda. + `; + case "pubsub": + return `Each time a query runs, the data is
sent to Google Cloud Pub/Sub.`; + case "kafta": + return `Each time a query runs, the data
is sent to Apache Kafka.`; + case "stdout": + return `Each time a query runs, the data is sent to
+ standard output (stdout) on the Fleet server.`; + case "": + return "Please configure a log destination."; + default: + return "No additional information is available about this log destination."; + } + }; + + return ( + + {readableLogDestination()} + + ); +}; + +export default LogDestinationIndicator; diff --git a/frontend/components/LogDestinationIndicator/index.ts b/frontend/components/LogDestinationIndicator/index.ts new file mode 100644 index 0000000000..1d2d5a12d4 --- /dev/null +++ b/frontend/components/LogDestinationIndicator/index.ts @@ -0,0 +1 @@ +export { default } from "./LogDestinationIndicator"; diff --git a/frontend/components/Modal/Modal.tsx b/frontend/components/Modal/Modal.tsx index 4044852d82..0ec2c2149f 100644 --- a/frontend/components/Modal/Modal.tsx +++ b/frontend/components/Modal/Modal.tsx @@ -11,6 +11,7 @@ export interface IModalProps { children: JSX.Element; onExit: () => void; onEnter?: () => void; + /** default 650px, large 800px, xlarge 850px, auto auto-width */ width?: ModalWidth; className?: string; } diff --git a/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx b/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx new file mode 100644 index 0000000000..df2a8fc374 --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.stories.tsx @@ -0,0 +1,18 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import QueryFrequencyIndicator from "./QueryFrequencyIndicator"; + +const meta: Meta = { + title: "Components/QueryFrequencyIndicator", + component: QueryFrequencyIndicator, + args: { + frequency: 300, + checked: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx b/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx new file mode 100644 index 0000000000..94dfecbdda --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/QueryFrequencyIndicator.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import classnames from "classnames"; +import Icon from "components/Icon/Icon"; +import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; + +interface IStatusIndicatorProps { + frequency: number; + checked: boolean; +} + +const generateClassTag = (rawValue: string): string => { + if (rawValue === DEFAULT_EMPTY_CELL_VALUE) { + return "indeterminate"; + } + return rawValue.replace(" ", "-").toLowerCase(); +}; + +const QueryFrequencyIndicator = ({ + frequency, + checked, +}: IStatusIndicatorProps): JSX.Element => { + const classTag = generateClassTag(frequency.toString()); + const frequencyClassName = classnames( + "query-frequency-indicator", + `query-frequency-indicator--${classTag}`, + `frequency--${classTag}` + ); + const readableQueryFrequency = () => { + switch (frequency) { + case 0: + return "Never"; + case 300: + case 600: + case 900: + case 1800: // 5, 10, 15, 30 minutes + return `${(frequency / 60).toString()} minutes`; + case 3600: + return "Hourly"; + case 21600: + case 43200: // 6, 12 hours + return `${(frequency / 3600).toString()} hours`; + case 86400: + return "Daily"; + case 604800: + return "Weekly"; + default: + return "Unknown"; + } + }; + + const frequencyIcon = () => { + if (frequency === 0) { + return checked ? ( + + ) : ( + + ); + } + return ; + }; + + return ( +
+ {frequencyIcon()} + {readableQueryFrequency()} +
+ ); +}; + +export default QueryFrequencyIndicator; diff --git a/frontend/components/QueryFrequencyIndicator/_styles.scss b/frontend/components/QueryFrequencyIndicator/_styles.scss new file mode 100644 index 0000000000..fb6624429f --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/_styles.scss @@ -0,0 +1,13 @@ +.query-frequency-indicator { + width: 100px; + display: flex; + padding: 8px 12px; + + .icon { + padding-right: $pad-small; + } +} + +.grey { + color: $ui-fleet-black-33; +} diff --git a/frontend/components/QueryFrequencyIndicator/index.ts b/frontend/components/QueryFrequencyIndicator/index.ts new file mode 100644 index 0000000000..4f84c00133 --- /dev/null +++ b/frontend/components/QueryFrequencyIndicator/index.ts @@ -0,0 +1 @@ +export { default } from "./QueryFrequencyIndicator"; diff --git a/frontend/components/icons/Clock.tsx b/frontend/components/icons/Clock.tsx new file mode 100644 index 0000000000..c739a19aa7 --- /dev/null +++ b/frontend/components/icons/Clock.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +import { COLORS, Colors } from "styles/var/colors"; +import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes"; + +interface IClockProps { + color?: Colors; + size?: IconSizes; +} + +const Clock = ({ + color = "ui-fleet-black-75", + size = "small", +}: IClockProps) => { + return ( + + + + + ); +}; + +export default Clock; diff --git a/frontend/components/icons/Warning.tsx b/frontend/components/icons/Warning.tsx new file mode 100644 index 0000000000..a3e1ce156c --- /dev/null +++ b/frontend/components/icons/Warning.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +import { COLORS, Colors } from "styles/var/colors"; +import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes"; + +interface IWarningProps { + color?: Colors; + size?: IconSizes; +} + +const Warning = ({ + color = "status-warning", + size = "small", +}: IWarningProps) => { + return ( + + + + ); +}; + +export default Warning; diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts index b42cfaa152..8f1747db5a 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -50,6 +50,8 @@ import Pending from "./Pending"; import PendingPartial from "./PendingPartial"; import ErrorOutline from "./ErrorOutline"; import Error from "./Error"; +import Warning from "./Warning"; +import Clock from "./Clock"; import Copy from "./Copy"; import Eye from "./Eye"; @@ -108,6 +110,8 @@ export const ICON_MAP = { "pending-partial": PendingPartial, error: Error, "error-outline": ErrorOutline, + warning: Warning, + clock: Clock, darwin: Apple, macOS: Apple, windows: Windows, diff --git a/frontend/interfaces/query.ts b/frontend/interfaces/query.ts index 96a8efa4d1..d6a948cd25 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -1,5 +1,6 @@ import { IFormField } from "./form_field"; import { IPack } from "./pack"; +import { ISchedulableQuery } from "./schedulable_query"; import { IScheduledQueryStats } from "./scheduled_query_stats"; export interface IQueryFormData { @@ -7,14 +8,15 @@ export interface IQueryFormData { name?: string | number | boolean | undefined; query?: string | number | boolean | undefined; observer_can_run?: string | number | boolean | undefined; + automations_enabled?: boolean; } export interface IStoredQueryResponse { - query: IQuery; + query: ISchedulableQuery; } export interface IFleetQueriesResponse { - queries: IQuery[]; + queries: ISchedulableQuery[]; } export interface IQuery { diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index b7673c25a3..9697ca1518 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -26,7 +26,8 @@ import useTeamIdParam from "hooks/useTeamIdParam"; import RevealButton from "components/buttons/RevealButton"; import QueriesTable from "./components/QueriesTable"; import DeleteQueryModal from "./components/DeleteQueryModal"; -import ManageAutomationsModal from "./components/ManageAutomationsModal"; +import ManageAutomationsModal from "./components/ManageAutomationsModal/ManageAutomationsModal"; +import PreviewDataModal from "./components/PreviewDataModal/PreviewDataModal"; const baseClass = "manage-queries-page"; interface IManageQueriesPageProps { @@ -85,6 +86,7 @@ const ManageQueriesPage = ({ filteredQueriesPath, isPremiumTier, isSandboxMode, + config, } = useContext(AppContext); const { setResetSelectedRows } = useContext(TableContext); @@ -107,11 +109,13 @@ const ManageQueriesPage = ({ const [selectedQueryIds, setSelectedQueryIds] = useState([]); const [showDeleteQueryModal, setShowDeleteQueryModal] = useState(false); - const [isUpdatingQueries, setIsUpdatingQueries] = useState(false); const [showManageAutomationsModal, setShowManageAutomationsModal] = useState( false ); + const [showPreviewDataModal, setShowPreviewDataModal] = useState(false); + const [isUpdatingQueries, setIsUpdatingQueries] = useState(false); const [showInheritedQueries, setShowInheritedQueries] = useState(false); + const [isUpdatingAutomations, setIsUpdatingAutomations] = useState(false); interface IQueryKeyQueriesLoadAll { scope: "enhancedQueries"; @@ -165,6 +169,14 @@ const ManageQueriesPage = ({ } ); + const automatedQueryIds = useMemo(() => { + return curTeamEnhancedQueries + ? curTeamEnhancedQueries + .filter((query) => query.automations_enabled) + .map((query) => query.id) + : []; + }, [curTeamEnhancedQueries]); + useEffect(() => { const path = location.pathname + location.search; if (filteredQueriesPath !== path) { @@ -178,10 +190,6 @@ const ManageQueriesPage = ({ setShowDeleteQueryModal(!showDeleteQueryModal); }, [showDeleteQueryModal, setShowDeleteQueryModal]); - const toggleManageAutomationsModal = useCallback(() => { - setShowManageAutomationsModal(!showManageAutomationsModal); - }, [showManageAutomationsModal, setShowManageAutomationsModal]); - const onDeleteQueryClick = (selectedTableQueryIds: number[]) => { toggleDeleteQueryModal(); setSelectedQueryIds(selectedTableQueryIds); @@ -192,6 +200,25 @@ const ManageQueriesPage = ({ refetchGlobalQueries(); }, [refetchCurTeamQueries, refetchGlobalQueries]); + const toggleManageAutomationsModal = useCallback(() => { + setShowManageAutomationsModal(!showManageAutomationsModal); + }, [showManageAutomationsModal, setShowManageAutomationsModal]); + + const onManageAutomationsClick = () => { + toggleManageAutomationsModal(); + }; + + const togglePreviewDataModal = useCallback(() => { + // Manage automation modal must close/open every time preview data modal opens/closes + setShowManageAutomationsModal(!showManageAutomationsModal); + setShowPreviewDataModal(!showPreviewDataModal); + }, [ + showPreviewDataModal, + setShowPreviewDataModal, + showManageAutomationsModal, + setShowManageAutomationsModal, + ]); + const onDeleteQuerySubmit = useCallback(async () => { const bulk = selectedQueryIds.length > 1; setIsUpdatingQueries(true); @@ -318,6 +345,52 @@ const ManageQueriesPage = ({ ); }; + const onSaveQueryAutomations = useCallback( + async (newAutomatedQueryIds) => { + setIsUpdatingAutomations(true); + + // Query ids added to turn on automations + const turnOnAutomations = newAutomatedQueryIds.filter( + (query: number) => !automatedQueryIds.includes(query) + ); + // Query ids removed to turn off automations + const turnOffAutomations = automatedQueryIds.filter( + (query: number) => !newAutomatedQueryIds.includes(query) + ); + + // Update query automations using queries/{id} manage_automations parameter + const updateAutomatedQueries = []; + updateAutomatedQueries.push( + turnOnAutomations.map((id: number) => + queriesAPI.update(id, { automations_enabled: true }) + ) + ); + updateAutomatedQueries.push( + turnOffAutomations.map((id: number) => + queriesAPI.update(id, { automations_enabled: false }) + ) + ); + + try { + await Promise.all(updateAutomatedQueries).then(() => { + renderFlash("success", `Successfully updated query automations.`); + refetchAllQueries(); + }); + } catch (errorResponse) { + renderFlash( + "error", + `There was an error updating your query automations. Please try again later.` + ); + } finally { + toggleManageAutomationsModal(); + setIsUpdatingAutomations(false); + } + }, + [refetchAllQueries, automatedQueryIds, toggleManageAutomationsModal] + ); + + // const isTableDataLoading = isFetchingFleetQueries || queriesList === null; + const renderModals = () => { return ( <> @@ -329,7 +402,18 @@ const ManageQueriesPage = ({ /> )} {showManageAutomationsModal && ( - + + )} + {showPreviewDataModal && ( + )} ); @@ -347,7 +431,7 @@ const ManageQueriesPage = ({
{(isGlobalAdmin || isTeamAdmin) && ( + <> + + )}
diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index cb6abd9cb8..b5cfea5899 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -1,19 +1,203 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; +import { omit } from "lodash"; import Modal from "components/Modal"; +import Button from "components/buttons/Button"; +import InfoBanner from "components/InfoBanner/InfoBanner"; +import CustomLink from "components/CustomLink/CustomLink"; +import Checkbox from "components/forms/fields/Checkbox/Checkbox"; +import QueryFrequencyIndicator from "components/QueryFrequencyIndicator/QueryFrequencyIndicator"; +import LogDestinationIndicator from "components/LogDestinationIndicator/LogDestinationIndicator"; -const baseClass = "automations-modal"; +import { ISchedulableQuery } from "interfaces/schedulable_query"; +interface IFrequencyIndicator { + frequency: number; + checked: boolean; +} interface IManageAutomationsModalProps { - onExit: () => void; + isUpdatingAutomations: boolean; + handleSubmit: (formData: any) => void; // TODO + onCancel: () => void; + togglePreviewDataModal: () => void; + availableQueries?: ISchedulableQuery[]; + automatedQueryIds: number[]; + logDestination: string; } +interface ICheckedQuery { + name?: string; + id: number; + isChecked: boolean; + interval: number; +} + +const useCheckboxListStateManagement = ( + allQueries: ISchedulableQuery[], + automatedQueryIds: number[] | undefined +) => { + const [queryItems, setQueryItems] = useState(() => { + return allQueries.map(({ name, id, interval }) => ({ + name, + id, + isChecked: !!automatedQueryIds?.includes(id), + interval, + })); + }); + + const updateQueryItems = (queryId: number) => { + setQueryItems((prevItems) => + prevItems.map((query) => + query.id !== queryId ? query : { ...query, isChecked: !query.isChecked } + ) + ); + }; + + return { queryItems, updateQueryItems }; +}; + +const baseClass = "manage-automations-modal"; + const ManageAutomationsModal = ({ - onExit, + isUpdatingAutomations, + automatedQueryIds, + handleSubmit, + onCancel, + togglePreviewDataModal, + availableQueries, + logDestination, }: IManageAutomationsModalProps): JSX.Element => { + // TODO: Error handling, if any + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + + const { queryItems, updateQueryItems } = useCheckboxListStateManagement( + availableQueries || [], + automatedQueryIds || [] + ); + + const onSubmit = (evt: React.MouseEvent | KeyboardEvent) => { + evt.preventDefault(); + + const newQueryIds: number[] = []; + queryItems?.forEach((p) => p.isChecked && newQueryIds.push(p.id)); + + handleSubmit(newQueryIds); + }; + + useEffect(() => { + const listener = (event: KeyboardEvent) => { + if (event.code === "Enter" || event.code === "NumpadEnter") { + event.preventDefault(); + onSubmit(event); + } + }; + document.addEventListener("keydown", listener); + return () => { + document.removeEventListener("keydown", listener); + }; + }); + return ( - -
+ +
+
+ Query automations let you send data to your log destination on a + schedule. Data is sent according to a query’s frequency. +
+ {availableQueries?.length ? ( +
+

+ Choose which queries will send data: +

+
+ {queryItems && + queryItems.map((queryItem) => { + const { isChecked, name, id, interval } = queryItem; + return ( +
+ { + updateQueryItems(id); + !isChecked && + setErrors((errs) => omit(errs, "queryItems")); + }} + > + {name} + + +
+ ); + })} +
+
+ ) : ( +
+ You have no queries. +

Add a query to turn on automations.

+
+ )} +
+

+ Log destination: +

+
+ +
+
+ Users with the admin role can  + +
+
+ + Automations currently run on macOS, Windows, and Linux hosts. +
+ Interested in query automations for your Chromebooks?   + +
+
+
+ +
+
+ + +
+
+
); }; diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss new file mode 100644 index 0000000000..296b36f7aa --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss @@ -0,0 +1,48 @@ +.manage-automations-modal { + display: flex; + flex-direction: column; + gap: $pad-xlarge; + + &__selection { + margin-bottom: $pad-small; + } + + &__checkboxes { + display: flex; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + border-radius: 4px; + border: 1px solid $ui-fleet-black-10; + } + + &__query-item { + width: 100%; + display: flex; + justify-content: space-between; + + &:not(:last-child) { + border-bottom: 1px solid $ui-fleet-black-10; + } + } + + .fleet-checkbox { + height: 20px; + + &__label { + width: 490px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .form-field--checkbox { + display: flex; + padding: 8px 12px; + justify-content: space-between; + align-items: center; + align-self: stretch; + margin-bottom: 0; + } +} diff --git a/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx new file mode 100644 index 0000000000..3156c73288 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx @@ -0,0 +1,61 @@ +/* This component is used for creating and editing both global and team scheduled queries */ + +import React from "react"; +import { syntaxHighlight } from "utilities/helpers"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; +import TooltipWrapper from "components/TooltipWrapper"; + +const baseClass = "preview-data-modal"; + +interface IPreviewDataModalProps { + onCancel: () => void; +} + +const PreviewDataModal = ({ + onCancel, +}: IPreviewDataModalProps): JSX.Element => { + const json = { + action: "snapshot", + snapshot: [ + { + remote_address: "0.0.0.0", + remote_port: "0", + cmdline: "/usr/sbin/syslogd", + }, + ], + name: "xxxxxxx", + hostIdentifier: "xxxxxxx", + calendarTime: "xxx xxx x xx:xx:xx xxxx UTC", + unixTime: "xxxxxxxxx", + epoch: "xxxxxxxxx", + counter: "x", + numerics: "x", + }; + + return ( + +
+

+ + The data sent to your configured log destination will look similar + to the following JSON: + +

+
+
+        
+
+ +
+
+
+ ); +}; + +export default PreviewDataModal; diff --git a/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts new file mode 100644 index 0000000000..48fca40136 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/index.ts @@ -0,0 +1 @@ +export { default } from "./PreviewDataModal"; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 8749d19e92..444edc55ae 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -205,6 +205,7 @@ const generateTableHeaders = ({ }, }, { + title: "Performance impact", Header: () => { return (
diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index 15a3b1a6b4..50ebcbf1f9 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -28,7 +28,11 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { "queries/2": RESPONSES.globalQuery2, "queries/3": RESPONSES.globalQuery3, "queries/4": RESPONSES.teamQuery1, - "queries?team_id=43": RESPONSES.teamQueries, + "queries/5": RESPONSES.globalQuery4, + "queries/6": RESPONSES.globalQuery5, + "queries/7": RESPONSES.globalQuery6, + "queries/8": RESPONSES.teamQuery2, + "queries?team_id=13": RESPONSES.teamQueries, }, POST: { // request body is ISelectedTargets diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index c36175938e..31edff807e 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -431,6 +431,60 @@ const globalQueries = { description: "A third test query (Select all from windows_crashes", query: "SELECT * FROM windows_crashes", team_id: null, + interval: 604800, // Weekly + platform: "", + min_osquery_version: "", + automations_enabled: true, + logging: "differential", + saved: false, + author_id: 2, + author_name: "Test User 2", + author_email: "test2@example.com", + observer_can_run: true, + packs: [], + stats: { + system_time_p50: null, + system_time_p95: null, + user_time_p50: null, + user_time_p95: null, + total_executions: null, + }, + }, + { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 5, + name: "Test Query 4 (Never runs)", + description: "A third test query", + query: "SELECT * FROM osquery_info", + team_id: 2, + interval: 0, // Never + platform: "", + min_osquery_version: "", + automations_enabled: true, + logging: "differential", + saved: false, + author_id: 2, + author_name: "Test User 2", + author_email: "test2@example.com", + observer_can_run: true, + packs: [], + stats: { + system_time_p50: null, + system_time_p95: null, + user_time_p50: null, + user_time_p95: null, + total_executions: null, + }, + }, + { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 6, + name: "Test Query 5 runs every 5 minutes!", + description: "A fifth test query", + query: "SELECT * FROM osquery_info", + team_id: 2, interval: 604800, // Every week platform: "Windows", min_osquery_version: "", @@ -450,6 +504,33 @@ const globalQueries = { total_executions: null, }, }, + { + created_at: "2022-11-03T17:22:14Z", + updated_at: "2022-11-03T17:22:14Z", + id: 7, + name: "Test Query 6 runs every 6 hours", + description: "A 6th test query", + query: "SELECT * FROM osquery_info", + team_id: null, + interval: 21600, // 6 hours + platform: "", + min_osquery_version: "", + automations_enabled: false, + logging: "snapshot", + saved: false, + author_id: 2, + author_name: "Test User", + author_email: "test@example.com", + observer_can_run: true, + packs: [], + stats: { + system_time_p50: null, + system_time_p95: null, + user_time_p50: null, + user_time_p95: null, + total_executions: null, + }, + }, ], }; @@ -459,6 +540,35 @@ const teamQueries = { created_at: "2023-06-08T15:31:35Z", updated_at: "2023-06-08T15:31:35Z", id: 4, + name: "test specific team query 2", + description: "", + query: "SELECT * FROM video_info;", + team_id: 13, + platform: "windows", + min_osquery_version: "", + automations_enabled: true, + logging: "snapshot", + saved: true, + interval: 0, + observer_can_run: true, + author_id: 1, + author_name: "Jacob", + author_email: "jacob@fleetdm.com", + packs: [], + stats: { + system_time_p50: 1, + // system_time_p95: null, + user_time_p50: 1, + // user_time_p95: null, + total_executions: 1, + }, + performance: "Undetermined", + platforms: ["windows"], + }, + { + created_at: "2023-06-08T15:31:35Z", + updated_at: "2023-06-08T15:31:35Z", + id: 8, name: "test specific team query", description: "", query: "SELECT * FROM osquery_info;", @@ -476,14 +586,14 @@ const teamQueries = { author_email: "jacob@fleetdm.com", packs: [], stats: { - system_time_p50: 1, + system_time_p50: 4, // system_time_p95: null, - user_time_p50: 1, + user_time_p50: 10, // user_time_p95: null, total_executions: 1, }, performance: "Undetermined", - platforms: ["windows", "darwin", "linux"], + platforms: ["darwin"], }, ], }; @@ -491,7 +601,11 @@ const teamQueries = { const globalQuery1 = { query: globalQueries.queries[0] }; const globalQuery2 = { query: globalQueries.queries[1] }; const globalQuery3 = { query: globalQueries.queries[2] }; +const globalQuery4 = { query: globalQueries.queries[4] }; +const globalQuery5 = { query: globalQueries.queries[5] }; +const globalQuery6 = { query: globalQueries.queries[6] }; const teamQuery1 = { query: teamQueries.queries[0] }; +const teamQuery2 = { query: teamQueries.queries[1] }; export default { count, @@ -501,6 +615,10 @@ export default { globalQuery1, globalQuery2, globalQuery3, + globalQuery4, + globalQuery5, + globalQuery6, teamQueries, teamQuery1, + teamQuery2, }; diff --git a/frontend/styles/var/colors.scss b/frontend/styles/var/colors.scss index 652b967043..a16b2fa847 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -11,6 +11,7 @@ $site-nav-on-hover: #0e1533; // UI $ui-fleet-black-75: #515774; $ui-fleet-black-50: #8b8fa2; +$ui-fleet-black-33: #b3b6c1; $ui-fleet-black-25: #c5c7d1; $ui-fleet-black-10: #e2e4ea; $ui-fleet-blue-10: #f9fafc;