Add ability to run live queries on new and existing policies (#3230)

This commit is contained in:
Luke Heath 2021-12-09 10:38:51 -06:00 committed by GitHub
parent b397b3a68e
commit 2abae381e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 961 additions and 29 deletions

View file

@ -0,0 +1 @@
* Add ability to run live queries on new and existing policies

View file

@ -3,4 +3,5 @@
border-radius: $border-radius;
border: 1px solid #d9d9fe;
background-color: $ui-vibrant-blue-10;
font-size: 14px;
}

View file

@ -4,6 +4,7 @@ import hostUserInterface, { IHostUser } from "./host_users";
import labelInterface, { ILabel } from "./label";
import packInterface, { IPack } from "./pack";
import softwareInterface, { ISoftware } from "./software";
import hostQueryResult from "./campaign";
import queryStatsInterface, { IQueryStats } from "./query_stats";
export default PropTypes.shape({
@ -58,6 +59,7 @@ export default PropTypes.shape({
display_text: PropTypes.string,
users: PropTypes.arrayOf(hostUserInterface),
policies: PropTypes.arrayOf(hostPolicyInterface),
query_results: PropTypes.arrayOf(hostQueryResult),
});
export interface IDeviceUser {
@ -83,6 +85,18 @@ export interface IPackStats {
type: string;
}
export interface IHostPolicyQuery {
id: number;
hostname: string;
status?: string;
}
export interface IHostPolicyQueryError {
host_hostname: string;
osquery_version: string;
error: string;
}
export interface IHost {
created_at: string;
updated_at: string;
@ -136,4 +150,5 @@ export interface IHost {
munki?: IMunkiData;
mdm?: IMDMData;
policies: IHostPolicy[];
query_results?: [];
}

View file

@ -97,16 +97,6 @@ const ManagePolicyPage = (managePoliciesPageProps: {
refetchOnWindowFocus: false,
});
const { data: fleetQueries } = useQuery(
["fleetQueries"],
() => fleetQueriesAPI.loadAll(),
{
select: (data) => data.queries,
refetchOnMount: false,
refetchOnWindowFocus: false,
}
);
// ===== local state
const [globalPolicies, setGlobalPolicies] = useState<
IPolicyStats[] | never[]

View file

@ -2,7 +2,6 @@ import React from "react";
import { noop } from "lodash";
import paths from "router/paths";
import Button from "components/buttons/Button";
import { IPolicyStats } from "interfaces/policy";
import { ITeam } from "interfaces/team";
import TableContainer from "components/TableContainer";

View file

@ -7,7 +7,6 @@ import { memoize } from "lodash";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import { IPolicyStats } from "interfaces/policy";
import PATHS from "router/paths";
import sortUtils from "utilities/sort";

View file

@ -50,9 +50,6 @@ const PolicyPage = ({
const {
selectedOsqueryTable,
setSelectedOsqueryTable,
lastEditedQueryName,
lastEditedQueryDescription,
lastEditedQueryBody,
setLastEditedQueryName,
setLastEditedQueryDescription,
setLastEditedQueryBody,
@ -195,7 +192,6 @@ const PolicyPage = ({
const step2Opts = {
baseClass,
selectedTargets: [...selectedTargets],
policyIdForEdit,
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
setSelectedTargets,

View file

@ -0,0 +1,63 @@
import React from "react";
import { noop } from "lodash";
import { IHostPolicyQueryError } from "interfaces/host";
import TableContainer from "components/TableContainer";
import {
generateTableHeaders,
generateDataSet,
} from "./PolicyQueriesErrorsTableConfig";
const baseClass = "policies-queries-list-wrapper";
const noPolicyQueries = "no-policy-queries";
interface IPoliciesListWrapperProps {
errorsList: IHostPolicyQueryError[];
isLoading: boolean;
resultsTitle?: string;
canAddOrRemovePolicy?: boolean;
}
const PoliciesListWrapper = ({
errorsList,
isLoading,
resultsTitle,
canAddOrRemovePolicy,
}: IPoliciesListWrapperProps): JSX.Element => {
const NoPolicyQueries = () => {
return (
<div className={`${noPolicyQueries}__inner`}>
<p>No hosts are online.</p>
</div>
);
};
return (
<div
className={`${baseClass} ${
canAddOrRemovePolicy ? "" : "hide-selection-column"
}`}
>
<TableContainer
resultsTitle={resultsTitle || "policies"}
columns={generateTableHeaders()}
data={generateDataSet(errorsList)}
isLoading={isLoading}
defaultSortHeader={"name"}
defaultSortDirection={"asc"}
manualSortBy
showMarkAllPages={false}
isAllPagesSelected={false}
disablePagination
primarySelectActionButtonVariant="text-icon"
primarySelectActionButtonIcon="delete"
primarySelectActionButtonText={"Delete"}
emptyComponent={NoPolicyQueries}
onQueryChange={noop}
disableCount
/>
</div>
);
};
export default PoliciesListWrapper;

View file

@ -0,0 +1,86 @@
/* eslint-disable react/prop-types */
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import { memoize } from "lodash";
// @ts-ignore
import TextCell from "components/TableContainer/DataTable/TextCell/TextCell";
import { IHostPolicyQueryError } from "interfaces/host";
import sortUtils from "utilities/sort";
// TODO functions for paths math e.g., path={PATHS.MANAGE_HOSTS + getParams(cellProps.row.original)}
interface IHeaderProps {
column: {
host: string;
isSortedDesc: boolean;
};
}
interface ICellProps {
cell: {
value: any;
};
row: {
original: IHostPolicyQueryError;
};
}
interface IDataColumn {
Header: ((props: IHeaderProps) => JSX.Element) | string;
Cell: (props: ICellProps) => JSX.Element;
title?: string;
accessor?: string;
disableHidden?: boolean;
disableSortBy?: boolean;
sortType?: string;
}
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
const generateTableHeaders = (): IDataColumn[] => {
const tableHeaders: IDataColumn[] = [
{
title: "Host",
Header: "Host",
disableSortBy: true,
accessor: "host_hostname",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
),
},
{
title: "OSQuery Version",
Header: "OSQuery Version",
disableSortBy: true,
accessor: "osquery_version",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
),
},
{
title: "Error",
Header: "Error",
disableSortBy: true,
accessor: "error",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
),
},
];
return tableHeaders;
};
const generateDataSet = memoize(
(
policyHostsErrorsList: IHostPolicyQueryError[] = []
): IHostPolicyQueryError[] => {
policyHostsErrorsList = policyHostsErrorsList.sort((a, b) =>
sortUtils.caseInsensitiveAsc(a.host_hostname, b.host_hostname)
);
return policyHostsErrorsList;
}
);
export { generateTableHeaders, generateDataSet };

View file

@ -0,0 +1,140 @@
.policies-queries-list-wrapper {
border-collapse: collapse;
a {
color: $core-vibrant-blue;
font-size: $x-small;
text-decoration: none;
}
&__wrapper {
border: 1px solid $ui-fleet-blue-15;
border-radius: 4px;
overflow: hidden;
margin-top: $pad-medium;
}
.table-container {
margin-top: $pad-small;
}
.table-container__header {
display: none;
}
thead {
background-color: $ui-off-white;
border-bottom: 1px solid $ui-fleet-blue-15;
th {
font-size: $x-small;
font-weight: $bold;
text-align: left;
padding: $pad-medium $pad-large;
}
.host_hostname__header {
width: 50%;
}
}
tbody td img {
width: 16px;
height: 16px;
vertical-align: sub;
padding-right: 4px;
}
&__th-pack-name {
padding-left: 0;
text-align: left;
}
&__select-all {
margin-bottom: 0;
}
&__empty-table {
text-align: center;
font-size: $x-small;
color: $core-fleet-black;
}
&__policy-count {
color: $core-fleet-black;
font-size: $x-small;
font-weight: $bold;
margin: 0 12px 0 0;
display: inline-block;
}
}
.no-policies {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-top: $pad-xxxlarge;
a {
color: $core-vibrant-blue;
font-size: $x-small;
text-decoration: none;
}
h1 {
font-size: $large;
font-weight: $regular;
line-height: normal;
letter-spacing: normal;
color: $core-fleet-black;
}
h2 {
font-size: $small;
font-weight: $bold;
margin: 0 0 $pad-large;
line-height: 20px;
color: $core-fleet-black;
}
&__inner {
display: flex;
flex-direction: column;
align-items: center;
h1 {
font-size: $small;
font-weight: $bold;
margin-bottom: $pad-medium;
}
img {
width: 322px;
}
p {
color: $core-fleet-black;
font-weight: $regular;
font-size: $x-small;
margin: 0;
margin-bottom: $pad-large;
}
}
&__inner-text {
width: 500px;
padding: $pad-xxlarge 0;
}
&__bullet-text {
width: 455px;
text-align: left;
}
}
.no-team-policy {
border: 1px solid #e2e4ea;
box-sizing: border-box;
border-radius: 8px;
}

View file

@ -0,0 +1 @@
export { default } from "./PolicyQueriesErrorsListWrapper";

View file

@ -0,0 +1,63 @@
import React from "react";
import { noop } from "lodash";
import { IHostPolicyQuery } from "interfaces/host";
import TableContainer from "components/TableContainer";
import {
generateTableHeaders,
generateDataSet,
} from "./PolicyQueriesTableConfig";
const baseClass = "policies-queries-list-wrapper";
const noPolicyQueries = "no-policy-queries";
interface IPoliciesListWrapperProps {
policyHostsList: IHostPolicyQuery[];
isLoading: boolean;
resultsTitle?: string;
canAddOrRemovePolicy?: boolean;
}
const PoliciesListWrapper = ({
policyHostsList,
isLoading,
resultsTitle,
canAddOrRemovePolicy,
}: IPoliciesListWrapperProps): JSX.Element => {
const NoPolicyQueries = () => {
return (
<div className={`${noPolicyQueries}__inner`}>
<p>No hosts are online.</p>
</div>
);
};
return (
<div
className={`${baseClass} ${
canAddOrRemovePolicy ? "" : "hide-selection-column"
}`}
>
<TableContainer
resultsTitle={resultsTitle || "policies"}
columns={generateTableHeaders()}
data={generateDataSet(policyHostsList)}
isLoading={isLoading}
defaultSortHeader={"name"}
defaultSortDirection={"asc"}
manualSortBy
showMarkAllPages={false}
isAllPagesSelected={false}
disablePagination
primarySelectActionButtonVariant="text-icon"
primarySelectActionButtonIcon="delete"
primarySelectActionButtonText={"Delete"}
emptyComponent={NoPolicyQueries}
onQueryChange={noop}
disableCount
/>
</div>
);
};
export default PoliciesListWrapper;

View file

@ -0,0 +1,89 @@
/* eslint-disable react/prop-types */
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import { memoize } from "lodash";
// @ts-ignore
import TextCell from "components/TableContainer/DataTable/TextCell/TextCell";
import { IHostPolicyQuery } from "interfaces/host";
import sortUtils from "utilities/sort";
import PassIcon from "../../../../../../assets/images/icon-check-circle-green-16x16@2x.png";
import FailIcon from "../../../../../../assets/images/icon-exclamation-circle-red-16x16@2x.png";
// TODO functions for paths math e.g., path={PATHS.MANAGE_HOSTS + getParams(cellProps.row.original)}
interface IHeaderProps {
column: {
host: string;
isSortedDesc: boolean;
};
}
interface ICellProps {
cell: {
value: any;
};
row: {
original: IHostPolicyQuery;
};
}
interface IDataColumn {
Header: ((props: IHeaderProps) => JSX.Element) | string;
Cell: (props: ICellProps) => JSX.Element;
title?: string;
accessor?: string;
disableHidden?: boolean;
disableSortBy?: boolean;
sortType?: string;
}
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
const generateTableHeaders = (): IDataColumn[] => {
const tableHeaders: IDataColumn[] = [
{
title: "Host",
Header: "Host",
disableSortBy: true,
accessor: "hostname",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
),
},
{
title: "Status",
Header: "Status",
disableSortBy: true,
accessor: "query_results",
Cell: (cellProps: ICellProps): JSX.Element => (
<>
{cellProps.cell.value.length ? (
<>
<img alt="host passing" src={PassIcon} />
<span className="header-icon-text">Yes</span>
</>
) : (
<>
<img alt="host passing" src={FailIcon} />
<span className="header-icon-text">No</span>
</>
)}
</>
),
},
];
return tableHeaders;
};
const generateDataSet = memoize(
(policyHostsList: IHostPolicyQuery[] = []): IHostPolicyQuery[] => {
policyHostsList = policyHostsList.sort((a, b) =>
sortUtils.caseInsensitiveAsc(a.hostname, b.hostname)
);
return policyHostsList;
}
);
export { generateTableHeaders, generateDataSet };

View file

@ -0,0 +1,140 @@
.policies-queries-list-wrapper {
border-collapse: collapse;
a {
color: $core-vibrant-blue;
font-size: $x-small;
text-decoration: none;
}
&__wrapper {
border: 1px solid $ui-fleet-blue-15;
border-radius: 4px;
overflow: hidden;
margin-top: $pad-medium;
}
.table-container {
margin-top: $pad-small;
}
.table-container__header {
display: none;
}
thead {
background-color: $ui-off-white;
border-bottom: 1px solid $ui-fleet-blue-15;
th {
font-size: $x-small;
font-weight: $bold;
text-align: left;
padding: $pad-medium $pad-large;
}
.hostname__header {
width: 70%;
}
}
tbody td img {
width: 16px;
height: 16px;
vertical-align: sub;
padding-right: 4px;
}
&__th-pack-name {
padding-left: 0;
text-align: left;
}
&__select-all {
margin-bottom: 0;
}
&__empty-table {
text-align: center;
font-size: $x-small;
color: $core-fleet-black;
}
&__policy-count {
color: $core-fleet-black;
font-size: $x-small;
font-weight: $bold;
margin: 0 12px 0 0;
display: inline-block;
}
}
.no-policies {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-top: $pad-xxxlarge;
a {
color: $core-vibrant-blue;
font-size: $x-small;
text-decoration: none;
}
h1 {
font-size: $large;
font-weight: $regular;
line-height: normal;
letter-spacing: normal;
color: $core-fleet-black;
}
h2 {
font-size: $small;
font-weight: $bold;
margin: 0 0 $pad-large;
line-height: 20px;
color: $core-fleet-black;
}
&__inner {
display: flex;
flex-direction: column;
align-items: center;
h1 {
font-size: $small;
font-weight: $bold;
margin-bottom: $pad-medium;
}
img {
width: 322px;
}
p {
color: $core-fleet-black;
font-weight: $regular;
font-size: $x-small;
margin: 0;
margin-bottom: $pad-large;
}
}
&__inner-text {
width: 500px;
padding: $pad-xxlarge 0;
}
&__bullet-text {
width: 455px;
text-align: left;
}
}
.no-team-policy {
border: 1px solid #e2e4ea;
box-sizing: border-box;
border-radius: 8px;
}

View file

@ -0,0 +1 @@
export { default } from "./PolicyQueriesListWrapper";

View file

@ -0,0 +1,268 @@
import React, { useState, useEffect } from "react";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import moment from "moment";
import classnames from "classnames";
import FileSaver from "file-saver";
import { get } from "lodash";
// @ts-ignore
import convertToCSV from "utilities/convert_to_csv"; // @ts-ignore
import { ICampaign } from "interfaces/campaign";
import { ITarget } from "interfaces/target";
import Button from "components/buttons/Button"; // @ts-ignore
import Spinner from "components/Spinner";
import TabsWrapper from "components/TabsWrapper";
import InfoBanner from "components/InfoBanner";
import PolicyQueryListWrapper from "../PolicyQueriesListWrapper/PolicyQueriesListWrapper";
import PolicyQueriesErrorsListWrapper from "../PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsListWrapper";
import DownloadIcon from "../../../../../../assets/images/icon-download-12x12@2x.png";
interface IQueryResultsProps {
campaign: ICampaign;
isQueryFinished: boolean;
policyName?: string;
onRunQuery: (evt: React.MouseEvent<HTMLButtonElement>) => void;
onStopQuery: (evt: React.MouseEvent<HTMLButtonElement>) => void;
setSelectedTargets: (value: ITarget[]) => void;
goToQueryEditor: () => void;
}
const baseClass = "query-results";
const CSV_TITLE = "New Policy";
const PAGE_TITLES = {
RUNNING: "Querying selected hosts",
FINISHED: "Query finished",
};
const NAV_TITLES = {
RESULTS: "Results",
ERRORS: "Errors",
};
const QueryResults = ({
campaign,
isQueryFinished,
policyName,
onRunQuery,
onStopQuery,
setSelectedTargets,
goToQueryEditor,
}: IQueryResultsProps): JSX.Element => {
const { hosts: hostsOnline, hosts_count: hostsCount, errors } =
campaign || {};
const totalHostsOnline = get(campaign, ["totals", "online"], 0);
const totalHostsOffline = get(campaign, ["totals", "offline"], 0);
const totalRowsCount = get(campaign, ["query_results", "length"], 0);
const onlineTotalText = `${totalRowsCount} result${
totalRowsCount === 1 ? "" : "s"
}`;
const errorsTotalText = `${errors?.length || 0} result${
errors?.length === 1 ? "" : "s"
}`;
const [pageTitle, setPageTitle] = useState<string>(PAGE_TITLES.RUNNING);
const [navTabIndex, setNavTabIndex] = useState(0);
useEffect(() => {
if (isQueryFinished) {
setPageTitle(PAGE_TITLES.FINISHED);
} else {
setPageTitle(PAGE_TITLES.RUNNING);
}
}, [isQueryFinished]);
const onExportQueryResults = (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
if (hostsOnline) {
const hostsExport = hostsOnline.map((host) => {
return {
hostname: host.hostname,
status:
host.query_results && host.query_results.length ? "yes" : "no",
};
});
const csv = convertToCSV(hostsExport);
const formattedTime = moment(new Date()).format("MM-DD-YY hh-mm-ss");
const filename = `${policyName || CSV_TITLE} (${formattedTime}).csv`;
const file = new global.window.File([csv], filename, {
type: "text/csv",
});
FileSaver.saveAs(file);
}
};
const onExportErrorsResults = (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
if (errors) {
const csv = convertToCSV(errors);
const formattedTime = moment(new Date()).format("MM-DD-YY hh-mm-ss");
const filename = `${
policyName || CSV_TITLE
} Errors (${formattedTime}).csv`;
const file = new global.window.File([csv], filename, {
type: "text/csv",
});
FileSaver.saveAs(file);
}
};
const onQueryDone = () => {
setSelectedTargets([]);
goToQueryEditor();
};
const renderTable = () => {
const emptyResults =
!hostsOnline || !hostsOnline.length || !hostsCount.successful;
const hasNoResultsYet = !isQueryFinished && emptyResults;
const finishedWithNoResults =
isQueryFinished && (!hostsCount.successful || emptyResults);
if (hasNoResultsYet) {
return (
<div className={`${baseClass}__loading-spinner`}>
<Spinner />
</div>
);
}
if (finishedWithNoResults) {
return (
<p className="no-results-message">
Your live query returned no results.
<span>
Expecting to see results? Check to see if the hosts you targeted
reported &ldquo;Online&rdquo; or check out the &ldquo;Errors&rdquo;
table.
</span>
</p>
);
}
return (
<div className={`${baseClass}__results-table-container`}>
<InfoBanner>
Host that responded with results are marked <strong>Yes</strong>.
Hosts that responded with no results are marked <strong>No</strong>.
</InfoBanner>
<Button
className={`${baseClass}__export-btn`}
onClick={onExportQueryResults}
variant="text-link"
>
<>
Export hosts <img alt="" src={DownloadIcon} />
</>
</Button>
<PolicyQueryListWrapper
isLoading={false}
policyHostsList={hostsOnline}
resultsTitle="hosts"
/>
</div>
);
};
const renderErrorsTable = () => {
return (
<div className={`${baseClass}__error-table-container`}>
<Button
className={`${baseClass}__export-btn`}
onClick={onExportErrorsResults}
variant="text-link"
>
<>
Export errors <img alt="" src={DownloadIcon} />
</>
</Button>
<PolicyQueriesErrorsListWrapper
isLoading={false}
errorsList={errors}
resultsTitle="errors"
/>
</div>
);
};
const renderFinishedButtons = () => (
<div className={`${baseClass}__btn-wrapper`}>
<Button
className={`${baseClass}__done-btn`}
onClick={onQueryDone}
variant="brand"
>
Done
</Button>
<Button
className={`${baseClass}__run-btn`}
onClick={onRunQuery}
variant="blue-green"
>
Run again
</Button>
</div>
);
const renderStopQueryButton = () => (
<div className={`${baseClass}__btn-wrapper`}>
<Button
className={`${baseClass}__stop-btn`}
onClick={onStopQuery}
variant="alert"
>
<>
<Spinner isInButton />
Stop
</>
</Button>
</div>
);
const firstTabClass = classnames("react-tabs__tab", "no-count", {
"errors-empty": !errors || errors?.length === 0,
});
return (
<div className={baseClass}>
<div className={`${baseClass}__wrapper`}>
<h1>{pageTitle}</h1>
<div className={`${baseClass}__text-wrapper`}>
<span className={`${baseClass}__text-online`}>
Online: {totalHostsOnline} hosts / {onlineTotalText}
</span>
<span className={`${baseClass}__text-offline`}>
Offline: {totalHostsOffline} hosts / 0 results
</span>
<span className={`${baseClass}__text-error`}>
Errors: {hostsCount.failed} hosts / {errorsTotalText}
</span>
</div>
</div>
{isQueryFinished ? renderFinishedButtons() : renderStopQueryButton()}
<TabsWrapper>
<Tabs selectedIndex={navTabIndex} onSelect={(i) => setNavTabIndex(i)}>
<TabList>
<Tab className={firstTabClass}>{NAV_TITLES.RESULTS}</Tab>
<Tab disabled={!errors?.length}>
{errors?.length > 0 && (
<span className="count">{errors.length}</span>
)}
{NAV_TITLES.ERRORS}
</Tab>
</TabList>
<TabPanel>{renderTable()}</TabPanel>
<TabPanel>{renderErrorsTable()}</TabPanel>
</Tabs>
</TabsWrapper>
</div>
);
};
export default QueryResults;

View file

@ -0,0 +1,79 @@
.query-results {
padding: $pad-xxxlarge $pad-xxlarge;
.info-banner {
margin: 2rem auto 1.25rem;
}
&__text-wrapper {
margin-top: 20px;
display: flex;
flex-direction: column;
font-size: $x-small;
span:not(:last-of-type) {
margin-bottom: $pad-small;
}
}
&__text-online {
&:before {
background-color: $ui-success;
border-radius: 100%;
content: " ";
display: inline-block;
height: 8px;
margin-right: $pad-small;
width: 8px;
}
}
&__text-offline {
&:before {
background-color: $ui-fleet-black-25;
border-radius: 100%;
content: " ";
display: inline-block;
height: 8px;
margin-right: $pad-small;
width: 8px;
}
}
&__text-error {
&:before {
background-color: $ui-error;
border-radius: 100%;
content: " ";
display: inline-block;
height: 8px;
margin-right: $pad-small;
width: 8px;
}
}
&__btn-wrapper {
margin-top: $pad-large;
margin-bottom: $pad-xxlarge;
display: flex;
align-items: center;
.button {
padding: $pad-small $pad-medium;
&:not(:last-of-type) {
margin-right: $pad-small;
}
}
}
&__export-btn {
img {
width: 13px;
height: 13px;
margin-left: 8px;
position: relative;
top: -2px;
}
}
}

View file

@ -15,7 +15,7 @@ import { ICampaign, ICampaignState } from "interfaces/campaign";
import { IPolicy } from "interfaces/policy";
import { ITarget } from "interfaces/target";
import QueryResults from "components/QueryResults";
import QueryResults from "../components/QueryResults";
interface IRunQueryProps {
storedPolicy: IPolicy | undefined;
@ -31,7 +31,7 @@ const RunQuery = ({
policyIdForEdit,
setSelectedTargets,
goToQueryEditor,
}: IRunQueryProps) => {
}: IRunQueryProps): JSX.Element => {
const dispatch = useDispatch();
const [isQueryFinished, setIsQueryFinished] = useState<boolean>(false);
@ -152,10 +152,9 @@ const RunQuery = ({
destroyCampaign();
try {
const isStoredQueryEdited = storedPolicy?.query !== lastEditedQueryBody;
// because we are not using the saved query id if user edits the SQL
const queryId = isStoredQueryEdited ? null : policyIdForEdit;
// we do not want to run a stored query,
// instead always run provided query
const queryId = null;
const returnedCampaign = await queryAPI.run({
query: lastEditedQueryBody,
queryId,
@ -205,14 +204,16 @@ const RunQuery = ({
}, []);
const { campaign } = campaignState;
return (
<QueryResults
campaign={campaign}
isQueryFinished={isQueryFinished}
onRunQuery={onRunQuery}
onStopQuery={onStopQuery}
isQueryFinished={isQueryFinished}
setSelectedTargets={setSelectedTargets}
goToQueryEditor={goToQueryEditor}
policyName={storedPolicy?.name}
/>
);
};

View file

@ -31,7 +31,6 @@ interface ITargetPillSelectorProps {
interface ISelectTargetsProps {
baseClass: string;
selectedTargets: ITarget[];
policyIdForEdit: number | null;
goToQueryEditor: () => void;
goToRunQuery: () => void;
setSelectedTargets: React.Dispatch<React.SetStateAction<ITarget[]>>;
@ -75,11 +74,10 @@ const TargetPillSelector = ({
const SelectTargets = ({
baseClass,
selectedTargets,
policyIdForEdit,
goToQueryEditor,
goToRunQuery,
setSelectedTargets,
}: ISelectTargetsProps) => {
}: ISelectTargetsProps): JSX.Element => {
const [targetsTotalCount, setTargetsTotalCount] = useState<number | null>(
null
);
@ -99,7 +97,7 @@ const SelectTargets = ({
() =>
targetsAPI.loadAll({
query: searchText,
queryId: policyIdForEdit,
queryId: null,
selected: formatSelectedTargetsForApi(selectedTargets) as any,
}),
{

View file

@ -17,7 +17,7 @@ import InputField from "components/forms/fields/InputField";
import QueryResultsRow from "components/queries/QueryResultsRow";
import Spinner from "components/Spinner";
import TabsWrapper from "components/TabsWrapper";
import DownloadIcon from "../../../assets/images/icon-download-12x12@2x.png";
import DownloadIcon from "../../../../../../assets/images/icon-download-12x12@2x.png";
interface IQueryResultsProps {
campaign: ICampaign;

View file

@ -0,0 +1 @@
export { default } from "./QueryResults";

View file

@ -16,7 +16,7 @@ import { IQuery } from "interfaces/query";
import { ITarget } from "interfaces/target";
// import { useLastEditedQueryInfo } from "../helpers";
import QueryResults from "components/QueryResults";
import QueryResults from "../components/QueryResults";
interface IRunQueryProps {
storedQuery: IQuery | undefined;

View file

@ -13,6 +13,7 @@ const updateCampaignStateFromResults = (campaign, { data }) => {
const errors = campaign.errors || [];
const hosts = campaign.hosts || [];
const { host, rows, error } = data;
host.query_results = rows;
const { hosts_count: hostsCount } = campaign;
const newHosts = [...hosts, host];
const newQueryResults = [...queryResults, ...rows];