Fleet UI: New manage query automations modal (#12747)

This commit is contained in:
RachelElysia
2023-07-18 17:10:45 -04:00
committed by GitHub
parent a45edfdffa
commit e13644d664
23 changed files with 826 additions and 33 deletions
+1
View File
@@ -0,0 +1 @@
- Users able to manage schedulable queries (new feature) with automations modal
+6 -6
View File
@@ -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&apos;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";
+1
View File
@@ -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";
+39
View File
@@ -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;
+33
View File
@@ -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;
+4
View File
@@ -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,
+4 -2
View File
@@ -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>
@@ -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 querys 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&nbsp;
<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? &nbsp;
<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>
);
};
@@ -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;
}
}
@@ -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 &quot;snapshot&quot; key includes the query&apos;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";
@@ -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,
};
+1
View File
@@ -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;