diff --git a/changes/issue-2713-policy-live-queries b/changes/issue-2713-policy-live-queries
new file mode 100644
index 0000000000..91900e55ce
--- /dev/null
+++ b/changes/issue-2713-policy-live-queries
@@ -0,0 +1 @@
+* Add ability to run live queries on new and existing policies
diff --git a/frontend/components/InfoBanner/_styles.scss b/frontend/components/InfoBanner/_styles.scss
index 72c324c8c3..9c7b41a371 100644
--- a/frontend/components/InfoBanner/_styles.scss
+++ b/frontend/components/InfoBanner/_styles.scss
@@ -3,4 +3,5 @@
border-radius: $border-radius;
border: 1px solid #d9d9fe;
background-color: $ui-vibrant-blue-10;
+ font-size: 14px;
}
diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts
index 0e8fc540c0..120e1b007b 100644
--- a/frontend/interfaces/host.ts
+++ b/frontend/interfaces/host.ts
@@ -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?: [];
}
diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
index e902d69fb6..260a62fa09 100644
--- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
@@ -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[]
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx
index 188c0ba9db..07cea031cf 100644
--- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx
@@ -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";
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesTableConfig.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesTableConfig.tsx
index 72eed6cdbd..ad1de9d0e8 100644
--- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesTableConfig.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesTableConfig.tsx
@@ -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";
diff --git a/frontend/pages/policies/PolicyPage/PolicyPage.tsx b/frontend/pages/policies/PolicyPage/PolicyPage.tsx
index 315f14f134..40e4e6ea0e 100644
--- a/frontend/pages/policies/PolicyPage/PolicyPage.tsx
+++ b/frontend/pages/policies/PolicyPage/PolicyPage.tsx
@@ -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,
diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsListWrapper.tsx b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsListWrapper.tsx
new file mode 100644
index 0000000000..09c55efb11
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsListWrapper.tsx
@@ -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 (
+
+ );
+ };
+
+ return (
+
+ );
+};
+
+export default PoliciesListWrapper;
diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsTableConfig.tsx b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsTableConfig.tsx
new file mode 100644
index 0000000000..23ebe22d72
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsTableConfig.tsx
@@ -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 => (
+
+ ),
+ },
+ {
+ title: "OSQuery Version",
+ Header: "OSQuery Version",
+ disableSortBy: true,
+ accessor: "osquery_version",
+ Cell: (cellProps: ICellProps): JSX.Element => (
+
+ ),
+ },
+ {
+ title: "Error",
+ Header: "Error",
+ disableSortBy: true,
+ accessor: "error",
+ Cell: (cellProps: ICellProps): JSX.Element => (
+
+ ),
+ },
+ ];
+ 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 };
diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/_styles.scss b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/_styles.scss
new file mode 100644
index 0000000000..a1f6e284b4
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/_styles.scss
@@ -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;
+}
diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/index.ts b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/index.ts
new file mode 100644
index 0000000000..1ea9e64757
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/index.ts
@@ -0,0 +1 @@
+export { default } from "./PolicyQueriesErrorsListWrapper";
diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesListWrapper.tsx b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesListWrapper.tsx
new file mode 100644
index 0000000000..292592c560
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesListWrapper.tsx
@@ -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 (
+
+ );
+ };
+
+ return (
+
+ );
+};
+
+export default PoliciesListWrapper;
diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesTableConfig.tsx b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesTableConfig.tsx
new file mode 100644
index 0000000000..cd221e22f0
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesTableConfig.tsx
@@ -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 => (
+
+ ),
+ },
+ {
+ title: "Status",
+ Header: "Status",
+ disableSortBy: true,
+ accessor: "query_results",
+ Cell: (cellProps: ICellProps): JSX.Element => (
+ <>
+ {cellProps.cell.value.length ? (
+ <>
+
+ Yes
+ >
+ ) : (
+ <>
+
+ No
+ >
+ )}
+ >
+ ),
+ },
+ ];
+ return tableHeaders;
+};
+
+const generateDataSet = memoize(
+ (policyHostsList: IHostPolicyQuery[] = []): IHostPolicyQuery[] => {
+ policyHostsList = policyHostsList.sort((a, b) =>
+ sortUtils.caseInsensitiveAsc(a.hostname, b.hostname)
+ );
+ return policyHostsList;
+ }
+);
+
+export { generateTableHeaders, generateDataSet };
diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/_styles.scss b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/_styles.scss
new file mode 100644
index 0000000000..fa1684be7b
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/_styles.scss
@@ -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;
+}
diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/index.ts b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/index.ts
new file mode 100644
index 0000000000..ba92ba90e6
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/index.ts
@@ -0,0 +1 @@
+export { default } from "./PolicyQueriesListWrapper";
diff --git a/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx b/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx
new file mode 100644
index 0000000000..a78213dfc5
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx
@@ -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) => void;
+ onStopQuery: (evt: React.MouseEvent) => 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(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) => {
+ 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) => {
+ 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 (
+
+
+
+ );
+ }
+
+ if (finishedWithNoResults) {
+ return (
+
+ Your live query returned no results.
+
+ Expecting to see results? Check to see if the hosts you targeted
+ reported “Online” or check out the “Errors”
+ table.
+
+
+ );
+ }
+
+ return (
+
+
+ Host that responded with results are marked Yes .
+ Hosts that responded with no results are marked No .
+
+
+ <>
+ Export hosts
+ >
+
+
+
+ );
+ };
+
+ const renderErrorsTable = () => {
+ return (
+
+
+ <>
+ Export errors
+ >
+
+
+
+ );
+ };
+
+ const renderFinishedButtons = () => (
+
+
+ Done
+
+
+ Run again
+
+
+ );
+
+ const renderStopQueryButton = () => (
+
+
+ <>
+
+ Stop
+ >
+
+
+ );
+
+ const firstTabClass = classnames("react-tabs__tab", "no-count", {
+ "errors-empty": !errors || errors?.length === 0,
+ });
+
+ return (
+
+
+
{pageTitle}
+
+
+ Online: {totalHostsOnline} hosts / {onlineTotalText}
+
+
+ Offline: {totalHostsOffline} hosts / 0 results
+
+
+ Errors: {hostsCount.failed} hosts / {errorsTotalText}
+
+
+
+ {isQueryFinished ? renderFinishedButtons() : renderStopQueryButton()}
+
+ setNavTabIndex(i)}>
+
+ {NAV_TITLES.RESULTS}
+
+ {errors?.length > 0 && (
+ {errors.length}
+ )}
+ {NAV_TITLES.ERRORS}
+
+
+ {renderTable()}
+ {renderErrorsTable()}
+
+
+
+ );
+};
+
+export default QueryResults;
diff --git a/frontend/pages/policies/PolicyPage/components/QueryResults/_styles.scss b/frontend/pages/policies/PolicyPage/components/QueryResults/_styles.scss
new file mode 100644
index 0000000000..633c208f9b
--- /dev/null
+++ b/frontend/pages/policies/PolicyPage/components/QueryResults/_styles.scss
@@ -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;
+ }
+ }
+}
diff --git a/frontend/components/QueryResults/index.ts b/frontend/pages/policies/PolicyPage/components/QueryResults/index.ts
similarity index 100%
rename from frontend/components/QueryResults/index.ts
rename to frontend/pages/policies/PolicyPage/components/QueryResults/index.ts
diff --git a/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx b/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx
index 2a52673012..19fb5e22f6 100644
--- a/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx
+++ b/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx
@@ -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(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 (
);
};
diff --git a/frontend/pages/policies/PolicyPage/screens/SelectTargets.tsx b/frontend/pages/policies/PolicyPage/screens/SelectTargets.tsx
index 3f93d4e8a2..44eb1ff859 100644
--- a/frontend/pages/policies/PolicyPage/screens/SelectTargets.tsx
+++ b/frontend/pages/policies/PolicyPage/screens/SelectTargets.tsx
@@ -31,7 +31,6 @@ interface ITargetPillSelectorProps {
interface ISelectTargetsProps {
baseClass: string;
selectedTargets: ITarget[];
- policyIdForEdit: number | null;
goToQueryEditor: () => void;
goToRunQuery: () => void;
setSelectedTargets: React.Dispatch>;
@@ -75,11 +74,10 @@ const TargetPillSelector = ({
const SelectTargets = ({
baseClass,
selectedTargets,
- policyIdForEdit,
goToQueryEditor,
goToRunQuery,
setSelectedTargets,
-}: ISelectTargetsProps) => {
+}: ISelectTargetsProps): JSX.Element => {
const [targetsTotalCount, setTargetsTotalCount] = useState(
null
);
@@ -99,7 +97,7 @@ const SelectTargets = ({
() =>
targetsAPI.loadAll({
query: searchText,
- queryId: policyIdForEdit,
+ queryId: null,
selected: formatSelectedTargetsForApi(selectedTargets) as any,
}),
{
diff --git a/frontend/components/QueryResults/QueryResults.tsx b/frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx
similarity index 99%
rename from frontend/components/QueryResults/QueryResults.tsx
rename to frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx
index c8d64ac82f..82b8651008 100644
--- a/frontend/components/QueryResults/QueryResults.tsx
+++ b/frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx
@@ -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;
diff --git a/frontend/components/QueryResults/_styles.scss b/frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss
similarity index 100%
rename from frontend/components/QueryResults/_styles.scss
rename to frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/index.ts b/frontend/pages/queries/QueryPage/components/QueryResults/index.ts
new file mode 100644
index 0000000000..4ff72f5be8
--- /dev/null
+++ b/frontend/pages/queries/QueryPage/components/QueryResults/index.ts
@@ -0,0 +1 @@
+export { default } from "./QueryResults";
diff --git a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx b/frontend/pages/queries/QueryPage/screens/RunQuery.tsx
index 45e04fae9e..7cfc32cddf 100644
--- a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx
+++ b/frontend/pages/queries/QueryPage/screens/RunQuery.tsx
@@ -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;
diff --git a/frontend/redux/nodes/entities/campaigns/helpers.js b/frontend/redux/nodes/entities/campaigns/helpers.js
index 64c65100dc..794f65d8e3 100644
--- a/frontend/redux/nodes/entities/campaigns/helpers.js
+++ b/frontend/redux/nodes/entities/campaigns/helpers.js
@@ -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];