Fleet UI: New manage query automations modal (#12747)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Users able to manage schedulable queries (new feature) with automations modal
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import LogDestinationIndicator from "./LogDestinationIndicator";
|
||||
|
||||
const meta: Meta<typeof LogDestinationIndicator> = {
|
||||
title: "Components/LogDestinationIndicator",
|
||||
component: LogDestinationIndicator,
|
||||
args: {
|
||||
logDestination: "filesystem",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof LogDestinationIndicator>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
@@ -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 <br />
|
||||
/var/log/osquery/osqueryd.snapshots.log <br />
|
||||
in each host's filesystem.`;
|
||||
case "firehose":
|
||||
return `Each time a query runs, the data is sent to <br />
|
||||
Amazon Kinesis Data Firehose.`;
|
||||
case "kinesis":
|
||||
return `Each time a query runs, the data is sent to <br />
|
||||
Amazon Kinesis Data Streams.`;
|
||||
case "lambda":
|
||||
return `
|
||||
Each time a query runs, the data <br />is sent to AWS Lambda.
|
||||
`;
|
||||
case "pubsub":
|
||||
return `Each time a query runs, the data is <br />sent to Google Cloud Pub/Sub.`;
|
||||
case "kafta":
|
||||
return `Each time a query runs, the data <br />is sent to Apache Kafka.`;
|
||||
case "stdout":
|
||||
return `Each time a query runs, the data is sent to <br />
|
||||
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 (
|
||||
<TooltipWrapper tipContent={tooltipText()} className={statusClassName}>
|
||||
{readableLogDestination()}
|
||||
</TooltipWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogDestinationIndicator;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./LogDestinationIndicator";
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import QueryFrequencyIndicator from "./QueryFrequencyIndicator";
|
||||
|
||||
const meta: Meta<typeof QueryFrequencyIndicator> = {
|
||||
title: "Components/QueryFrequencyIndicator",
|
||||
component: QueryFrequencyIndicator,
|
||||
args: {
|
||||
frequency: 300,
|
||||
checked: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof QueryFrequencyIndicator>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
@@ -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 ? (
|
||||
<Icon size="small" name="warning" />
|
||||
) : (
|
||||
<Icon size="small" name="clock" color="ui-fleet-black-33" />
|
||||
);
|
||||
}
|
||||
return <Icon size="small" name="clock" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${frequencyClassName}
|
||||
${frequency === 0 && !checked && "grey"}`}
|
||||
>
|
||||
{frequencyIcon()}
|
||||
{readableQueryFrequency()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryFrequencyIndicator;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./QueryFrequencyIndicator";
|
||||
@@ -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 (
|
||||
<svg
|
||||
width={ICON_SIZES[size]}
|
||||
height={ICON_SIZES[size]}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 13"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6 11a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9Zm0 1.5a6 6 0 1 0 0-12 6 6 0 0 0 0 12Z"
|
||||
fill={COLORS[color]}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6 3.125a.75.75 0 0 1 .75.75V5.75h1.125a.75.75 0 0 1 0 1.5H6a.75.75 0 0 1-.75-.75V3.875a.75.75 0 0 1 .75-.75Z"
|
||||
fill={COLORS[color]}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Clock;
|
||||
@@ -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 (
|
||||
<svg
|
||||
width={ICON_SIZES[size]}
|
||||
height={ICON_SIZES[size]}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 13"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="m11.887 11.214.008-.008L6.645.92l-.007.009C6.51.67 6.277.5 6 .5c-.277 0-.503.171-.638.429L5.356.92.105 11.206l.008.008a.898.898 0 0 0-.113.429c0 .471.338.857.75.857h10.5c.412 0 .75-.386.75-.857 0-.163-.045-.3-.113-.429ZM6 4.25a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-1.5 0V5A.75.75 0 0 1 6 4.25ZM6 11a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
|
||||
fill={COLORS[color]}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Warning;
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<number[]>([]);
|
||||
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 && (
|
||||
<ManageAutomationsModal onExit={toggleManageAutomationsModal} />
|
||||
<ManageAutomationsModal
|
||||
isUpdatingAutomations={isUpdatingAutomations}
|
||||
handleSubmit={onSaveQueryAutomations}
|
||||
onCancel={toggleManageAutomationsModal}
|
||||
togglePreviewDataModal={togglePreviewDataModal}
|
||||
availableQueries={curTeamEnhancedQueries}
|
||||
automatedQueryIds={automatedQueryIds}
|
||||
logDestination={config?.logging.result.plugin || ""}
|
||||
/>
|
||||
)}
|
||||
{showPreviewDataModal && (
|
||||
<PreviewDataModal onCancel={togglePreviewDataModal} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -347,7 +431,7 @@ const ManageQueriesPage = ({
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
{(isGlobalAdmin || isTeamAdmin) && (
|
||||
<Button
|
||||
onClick={toggleManageAutomationsModal}
|
||||
onClick={onManageAutomationsClick}
|
||||
className={`${baseClass}__manage-automations button`}
|
||||
variant="inverse"
|
||||
>
|
||||
@@ -356,13 +440,15 @@ const ManageQueriesPage = ({
|
||||
)}
|
||||
{(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) &&
|
||||
!!curTeamEnhancedQueries?.length && (
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${baseClass}__create-button`}
|
||||
onClick={onCreateQueryClick}
|
||||
>
|
||||
Add query
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${baseClass}__create-button`}
|
||||
onClick={onCreateQueryClick}
|
||||
>
|
||||
Add query
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+190
-6
@@ -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<ICheckedQuery[]>(() => {
|
||||
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<HTMLFormElement> | 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 (
|
||||
<Modal title={"Manage automations"} onExit={onExit} className={baseClass}>
|
||||
<div className={baseClass} />
|
||||
<Modal
|
||||
title={"Manage automations"}
|
||||
onExit={onCancel}
|
||||
className={baseClass}
|
||||
width="large"
|
||||
>
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__heading`}>
|
||||
Query automations let you send data to your log destination on a
|
||||
schedule. Data is sent according to a query’s frequency.
|
||||
</div>
|
||||
{availableQueries?.length ? (
|
||||
<div className={`${baseClass}__select`}>
|
||||
<p>
|
||||
<strong>Choose which queries will send data:</strong>
|
||||
</p>
|
||||
<div className={`${baseClass}__checkboxes`}>
|
||||
{queryItems &&
|
||||
queryItems.map((queryItem) => {
|
||||
const { isChecked, name, id, interval } = queryItem;
|
||||
return (
|
||||
<div key={id} className={`${baseClass}__query-item`}>
|
||||
<Checkbox
|
||||
value={isChecked}
|
||||
name={name}
|
||||
onChange={() => {
|
||||
updateQueryItems(id);
|
||||
!isChecked &&
|
||||
setErrors((errs) => omit(errs, "queryItems"));
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Checkbox>
|
||||
<QueryFrequencyIndicator
|
||||
frequency={interval}
|
||||
checked={isChecked}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${baseClass}__no-queries`}>
|
||||
<b>You have no queries.</b>
|
||||
<p>Add a query to turn on automations.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${baseClass}__log-destination`}>
|
||||
<p>
|
||||
<strong>Log destination:</strong>
|
||||
</p>
|
||||
<div className={`${baseClass}__selection`}>
|
||||
<LogDestinationIndicator logDestination={logDestination} />
|
||||
</div>
|
||||
<div className={`${baseClass}__configure`}>
|
||||
Users with the admin role can
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/docs/using-fleet/log-destinations"
|
||||
text="configure a different log destination"
|
||||
newTab
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InfoBanner className={`${baseClass}__supported-platforms`}>
|
||||
Automations currently run on macOS, Windows, and Linux hosts.
|
||||
<br />
|
||||
Interested in query automations for your Chromebooks?
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/contact"
|
||||
text="Let us know"
|
||||
newTab
|
||||
/>
|
||||
</InfoBanner>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<div className={`${baseClass}__preview-btn-wrap`}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="inverse"
|
||||
onClick={togglePreviewDataModal}
|
||||
>
|
||||
Preview data
|
||||
</Button>
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={onSubmit}
|
||||
className="save-loading"
|
||||
isLoading={isUpdatingAutomations}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+48
@@ -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;
|
||||
}
|
||||
}
|
||||
+61
@@ -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 (
|
||||
<Modal title={"Example data"} onExit={onCancel} className={baseClass}>
|
||||
<div className={`${baseClass}__preview-modal`}>
|
||||
<p>
|
||||
<TooltipWrapper
|
||||
tipContent={`The "snapshot" key includes the query's results. These will be unique to your query.`}
|
||||
>
|
||||
The data sent to your configured log destination will look similar
|
||||
to the following JSON:
|
||||
</TooltipWrapper>
|
||||
</p>
|
||||
<div className={`${baseClass}__host-status-webhook-preview`}>
|
||||
<pre dangerouslySetInnerHTML={{ __html: syntaxHighlight(json) }} />
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="brand">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewDataModal;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./PreviewDataModal";
|
||||
+1
@@ -205,6 +205,7 @@ const generateTableHeaders = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Performance impact",
|
||||
Header: () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user