@@ -131,7 +132,7 @@ const HostQueryReport = ({
iconStroke
>
<>
- View full query report
+ View data for all hosts
>
@@ -148,8 +149,10 @@ const HostQueryReport = ({
<>
JSX.Element) | string;
- accessor: string;
- Cell:
- | ((props: ICellProps) => JSX.Element)
- | ((props: IPerformanceImpactCell) => JSX.Element);
- disableHidden?: boolean;
- disableSortBy?: boolean;
-}
-
-interface IPackTable extends Partial {
- frequency: string;
- last_run: string;
- performance: { indicator: string; id: number };
-}
-
-// NOTE: cellProps come from react-table
-// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
-const generatePackTableHeaders = (): IDataColumn[] => {
- return [
- {
- title: "Query",
- Header: "Query",
- disableSortBy: true,
- accessor: "query_name",
- Cell: (cellProps: ICellProps) => (
-
- ),
- },
- {
- title: "Frequency",
- Header: "Frequency",
- disableSortBy: true,
- accessor: "frequency",
- Cell: (cellProps: ICellProps) => (
-
- ),
- },
- {
- Header: () => {
- return (
-
- The last time the query ran
-
- since the last time osquery
- started on this host.
- >
- }
- >
- Last run
-
- );
- },
- disableSortBy: true,
- accessor: "last_run",
- Cell: (cellProps: ICellProps) => (
-
- ),
- },
- {
- Header: () => {
- return (
-
- This is the performance
- impact on this host.
- >
- }
- >
- Performance impact
-
- );
- },
- disableSortBy: true,
- accessor: "performance",
- Cell: (cellProps: IPerformanceImpactCell) => (
-
- ),
- },
- ];
-};
-
-const enhancePackData = (query_stats: IQueryStats[]): IPackTable[] => {
- return Object.values(query_stats).map((query) => {
- const scheduledQueryPerformance = {
- user_time_p50: query.user_time,
- system_time_p50: query.system_time,
- total_executions: query.executions,
- };
- return {
- query_name: query.query_name,
- last_executed: query.last_executed,
- frequency: secondsToHms(query.interval),
- last_run: humanQueryLastRun(query.last_executed),
- performance: {
- indicator: getPerformanceImpactDescription(scheduledQueryPerformance),
- id: query.scheduled_query_id || parseInt(uniqueId(), 10),
- },
- };
- });
-};
-
-const generatePackDataSet = (query_stats: IQueryStats[]): IPackTable[] => {
- if (!query_stats) {
- return query_stats;
- }
-
- return [...enhancePackData(query_stats)];
-};
-
-export { generatePackTableHeaders, generatePackDataSet };
diff --git a/frontend/pages/hosts/details/cards/Packs/Packs.tsx b/frontend/pages/hosts/details/cards/Packs/Packs.tsx
deleted file mode 100644
index 2d4cd23c00..0000000000
--- a/frontend/pages/hosts/details/cards/Packs/Packs.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from "react";
-
-import { IPackStats } from "interfaces/host";
-import TableContainer from "components/TableContainer";
-import Card from "components/Card";
-import CardHeader from "components/CardHeader";
-
-import {
- Accordion,
- AccordionItem,
- AccordionItemHeading,
- AccordionItemButton,
- AccordionItemPanel,
-} from "react-accessible-accordion";
-
-import {
- generatePackTableHeaders,
- generatePackDataSet,
-} from "./PackTable/PackTableConfig";
-
-const baseClass = "schedule-card";
-
-interface IPacksProps {
- packsState?: IPackStats[];
- isLoading: boolean;
-}
-
-const Packs = ({ packsState, isLoading }: IPacksProps): JSX.Element => {
- const packs = packsState;
- const wrapperClassName = `${baseClass}__pack-table`;
- const tableHeaders = generatePackTableHeaders();
-
- let packsAccordion;
- if (packs) {
- packsAccordion = packs.map((pack) => {
- return (
-
-
- {pack.pack_name}
-
-
- {pack.query_stats.length === 0 ? (
- There are no schedule queries for this pack.
- ) : (
- <>
- {!!pack.query_stats.length && (
-
-
null}
- resultsTitle="queries"
- defaultSortHeader="scheduled_query_name"
- defaultSortDirection="asc"
- showMarkAllPages={false}
- isAllPagesSelected={false}
- emptyComponent={() => <>>}
- disablePagination
- disableCount
- />
-
- )}
- >
- )}
-
-
- );
- });
- }
-
- return !packs || !packs.length ? (
- <>>
- ) : (
-
-
-
- {packsAccordion}
-
-
- );
-};
-
-export default Packs;
diff --git a/frontend/pages/hosts/details/cards/Packs/_styles.scss b/frontend/pages/hosts/details/cards/Packs/_styles.scss
deleted file mode 100644
index f5d0326ecf..0000000000
--- a/frontend/pages/hosts/details/cards/Packs/_styles.scss
+++ /dev/null
@@ -1,108 +0,0 @@
-.card--packs {
- .table-container__header {
- display: none;
- }
-
- .data-table-block {
- .data-table__table {
- thead {
- .query_name__header {
- width: $col-lg;
- }
- .frequency__header {
- width: $col-md;
- }
- .last_run__header {
- display: none;
- width: 0;
- }
- @media (min-width: $break-md) {
- .last_run__header {
- display: table-cell;
- }
- }
- }
- tbody {
- .query_name__cell {
- width: $col-lg;
- }
- .frequency__cell {
- width: $col-md;
- }
- .last_run__cell {
- display: none;
- width: 0;
- }
- @media (min-width: $break-md) {
- .last_run__cell {
- display: table-cell;
- }
- }
- }
- }
- }
-
- .accordion {
- border-radius: 2px;
-
- .accordion__item + .accordion__item {
- border-top: 1px solid rgba(0, 0, 0, 0.1);
- }
-
- &__button {
- background-color: #fff;
- color: $core-fleet-black;
- cursor: pointer;
- text-align: left;
- font-size: $x-small;
- font-weight: $bold;
- border: none;
- padding: 17px 12px;
-
- &:hover {
- background-color: $ui-fleet-black-10;
- }
-
- &:after {
- display: block;
- content: url("../assets/images/icon-chevron-purple-9x6@2x.png");
- text-align: center;
- top: 50%;
- float: right;
- width: 32px;
- height: 32px;
- border-radius: 4px;
- transform: scale(0.5) translate(40%, -40%);
- }
-
- &[aria-expanded="true"]::after,
- &[aria-selected="true"]::after {
- background-color: $core-vibrant-blue;
- content: url("../assets/images/icon-accordion-collapse-16x16@2x.png");
- }
- }
-
- [hidden] {
- display: none;
- }
-
- &__panel {
- padding: 0;
- animation: fadein 0.35s ease-in;
- }
-
- /* -------------------------------------------------- */
- /* ---------------- Animation part ------------------ */
- /* -------------------------------------------------- */
-
- @keyframes fadein {
- 0% {
- opacity: 0;
- }
-
- 100% {
- opacity: 1;
- }
- }
- }
-}
diff --git a/frontend/pages/hosts/details/cards/Packs/index.ts b/frontend/pages/hosts/details/cards/Packs/index.ts
deleted file mode 100644
index 5ce4f55582..0000000000
--- a/frontend/pages/hosts/details/cards/Packs/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./Packs";
diff --git a/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx
index 640efea06a..dff268938c 100644
--- a/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx
+++ b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx
@@ -1,12 +1,14 @@
import React, { useCallback, useMemo } from "react";
-import { isAndroid } from "interfaces/platform";
+import { isAndroid, HostPlatform } from "interfaces/platform";
import { IQueryStats } from "interfaces/query_stats";
import { SUPPORT_LINK } from "utilities/constants";
import TableContainer from "components/TableContainer";
-import EmptyTable from "components/EmptyTable";
+import Card from "components/Card";
+import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
import CardHeader from "components/CardHeader";
+import Icon from "components/Icon";
import PATHS from "router/paths";
import { InjectedRouter } from "react-router";
import { Row } from "react-table";
@@ -17,13 +19,16 @@ import {
} from "./HostQueriesTableConfig";
const baseClass = "host-queries-card";
+const PAGE_SIZE = 4;
interface IHostQueriesProps {
hostId: number;
schedule?: IQueryStats[];
- hostPlatform: string;
+ hostPlatform: HostPlatform;
queryReportsDisabled?: boolean;
router: InjectedRouter;
+ canAddQuery?: boolean;
+ onClickAddQuery: () => void;
}
interface IHostQueriesRowProps extends Row {
@@ -34,74 +39,49 @@ interface IHostQueriesRowProps extends Row {
};
}
+type EmptyHostQueriesProps = {
+ hostPlatform: HostPlatform;
+};
+
+const EmptyHostQueries = ({ hostPlatform }: EmptyHostQueriesProps) => {
+ const platformActions: Record = {
+ chrome: "collecting data from your Chromebooks",
+ ios: "querying iPhones",
+ ipados: "querying iPads",
+ android: "querying Android hosts",
+ };
+
+ const action = platformActions[hostPlatform];
+
+ if (action) {
+ return (
+
+
Queries not supported for this host
+
+ Interested in {action}?{" "}
+
+
+
+ );
+ }
+
+ return (
+
+
No queries
+
Add a query to view custom vitals.
+
+ );
+};
+
const HostQueries = ({
hostId,
schedule,
hostPlatform,
queryReportsDisabled,
router,
+ canAddQuery,
+ onClickAddQuery,
}: IHostQueriesProps): JSX.Element => {
- const renderEmptyQueriesTab = () => {
- if (hostPlatform === "chrome") {
- return (
-
- Interested in collecting data from your Chromebooks?
-
- >
- }
- />
- );
- }
-
- if (hostPlatform === "ios" || hostPlatform === "ipados") {
- return (
-
- Interested in querying{" "}
- {hostPlatform === "ios" ? "iPhones" : "iPads"}?{" "}
-
- >
- }
- />
- );
- }
-
- if (isAndroid(hostPlatform)) {
- return (
-
- Interested in querying Android hosts?{" "}
-
- >
- }
- />
- );
- }
-
- return (
-
- Expecting to see queries? Try selecting Refetch to ask this
- host to report fresh vitals.
- >
- }
- />
- );
- };
-
const onSelectSingleRow = useCallback(
(row: IHostQueriesRowProps) => {
const { id: queryId, should_link_to_hqr } = row.original;
@@ -127,38 +107,48 @@ const HostQueries = ({
!schedule.length ||
hostPlatform === "chrome" ||
hostPlatform === "ios" ||
- hostPlatform === "ipados"
+ hostPlatform === "ipados" ||
+ isAndroid(hostPlatform)
) {
- return renderEmptyQueriesTab();
+ return ;
}
return (
-
-
null}
- resultsTitle="queries"
- defaultSortHeader="query_name"
- defaultSortDirection="asc"
- showMarkAllPages={false}
- isAllPagesSelected={false}
- emptyComponent={() => <>>}
- disablePagination
- disableCount
- disableMultiRowSelect={!queryReportsDisabled} // Removes hover/click state if reports are disabled
- isLoading={false} // loading state handled at parent level
- onSelectSingleRow={onSelectSingleRow}
- />
-
+ null}
+ resultsTitle="queries"
+ defaultSortHeader="query_name"
+ defaultSortDirection="asc"
+ showMarkAllPages={false}
+ isAllPagesSelected={false}
+ emptyComponent={() => <>>}
+ disablePagination={tableData.length <= PAGE_SIZE}
+ pageSize={PAGE_SIZE}
+ isClientSidePagination
+ disableCount
+ disableMultiRowSelect={!queryReportsDisabled} // Removes hover/click state if reports are disabled
+ isLoading={false} // loading state handled at parent level
+ onSelectSingleRow={onSelectSingleRow}
+ />
);
};
return (
-
-
+
+
+
+ {canAddQuery && (
+
+ )}
+
+
{renderHostQueries()}
-
+
);
};
diff --git a/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx b/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx
index b7eee6ddf7..65d1b6d830 100644
--- a/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx
+++ b/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx
@@ -1,18 +1,13 @@
import React from "react";
import { IQueryStats } from "interfaces/query_stats";
-import { getPerformanceImpactDescription } from "utilities/helpers";
import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell";
-import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
+import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
import TooltipWrapper from "components/TooltipWrapper";
import ReportUpdatedCell from "pages/hosts/details/cards/Queries/ReportUpdatedCell";
-import Icon from "components/Icon";
-import { Link } from "react-router";
-import PATHS from "router/paths";
interface IHostQueriesTableData extends Partial {
- performance: { indicator: string; id: number };
should_link_to_hqr: boolean;
id: number;
}
@@ -64,69 +59,34 @@ const generateColumnConfigs = (
): IDataColumn[] => {
const cols: IDataColumn[] = [
{
- title: "Query",
- Header: "Query",
- disableSortBy: true,
accessor: "query_name",
Cell: (cellProps: ICellProps) => (
),
+ Header: (cellProps) => (
+
+ ),
sortType: "caseInsensitive",
},
- {
- Header: () => {
- return (
-
- This is the performance
- impact on this host.
- >
- }
- >
- Performance impact
-
- );
- },
- disableSortBy: true,
- accessor: "performance",
- Cell: (cellProps: IPerformanceImpactCell) => {
- const baseClass = "performance-cell";
- const queryId = cellProps.row.original.id;
- return (
-
-
- {!queryReportsDisabled &&
- cellProps.row.original.should_link_to_hqr &&
- hostId &&
- queryId && (
- // parent row has same onClick functionality but link here is required for keyboard accessibility
-
-
-
- )}
-
- );
- },
- },
];
// include the Report updated column if query reports are globally enabled
if (!queryReportsDisabled) {
cols.push({
- Header: "Report updated",
+ Header: () => {
+ return (
+
+ Each query is updated based on an
+ individually set interval.
+ >
+ }
+ >
+ Last updated
+
+ );
+ },
disableSortBy: true,
accessor: "last_fetched", // tbd - may change
Cell: (cellProps: ICellProps) => {
@@ -148,9 +108,6 @@ const enhanceScheduleData = (
): IHostQueriesTableData[] => {
return Object.values(query_stats).map((query) => {
const {
- user_time,
- system_time,
- executions,
query_name,
scheduled_query_id,
last_fetched,
@@ -158,20 +115,9 @@ const enhanceScheduleData = (
discard_data,
automations_enabled,
} = query;
- // getPerformanceImpactDescription takes aggregate p50 values
- // getPerformanceImpactDescription takes aggregate p50 values so we need to divide by total executions in order to show average performance per query execution
- const scheduledQueryPerformance = {
- user_time_p50: executions > 0 ? user_time / executions : 0,
- system_time_p50: executions > 0 ? system_time / executions : 0,
- total_executions: executions,
- };
return {
query_name,
id: scheduled_query_id,
- performance: {
- indicator: getPerformanceImpactDescription(scheduledQueryPerformance),
- id: scheduled_query_id,
- },
last_fetched,
interval,
discard_data,
diff --git a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx
index 15434ed31b..b75c905bb4 100644
--- a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx
+++ b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx
@@ -60,7 +60,7 @@ describe("ReportUpdatedCell component", () => {
expect(screen.getByText(HUMAN_READABLE_DATETIME_REGEX)).toBeInTheDocument();
expect(screen.getByText(/\d+.+ago/)).toBeInTheDocument();
- expect(screen.getByText(/View report/)).toBeInTheDocument();
+ expect(screen.getByText(/View data/)).toBeInTheDocument();
});
it("Renders a last-updated timestamp with tooltip and link to report when a last_fetched date is present but not currently running an interval", () => {
const tenDaysAgo = new Date();
@@ -79,6 +79,6 @@ describe("ReportUpdatedCell component", () => {
expect(screen.getByText(HUMAN_READABLE_DATETIME_REGEX)).toBeInTheDocument();
expect(screen.getByText(/\d+.+ago/)).toBeInTheDocument();
- expect(screen.getByText(/View report/)).toBeInTheDocument();
+ expect(screen.getByText(/View data/)).toBeInTheDocument();
});
});
diff --git a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx
index 1db7f44f24..24d1d9a583 100644
--- a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx
+++ b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx
@@ -115,7 +115,7 @@ const ReportUpdatedCell = ({
);
};
- const onClick = (): void => {
+ const onClick = () => {
hostId &&
queryId &&
browserHistory.push(PATHS.HOST_QUERY_REPORT(hostId, queryId));
@@ -132,7 +132,7 @@ const ReportUpdatedCell = ({
onClick={onClick}
size="small"
>
- View report
+ View data
)}
diff --git a/frontend/pages/hosts/details/cards/Queries/_styles.scss b/frontend/pages/hosts/details/cards/Queries/_styles.scss
index c983c74a80..c750d91381 100644
--- a/frontend/pages/hosts/details/cards/Queries/_styles.scss
+++ b/frontend/pages/hosts/details/cards/Queries/_styles.scss
@@ -1,6 +1,16 @@
.host-queries-card {
@include vertical-page-tab-panel-layout;
+ // prevent layout shift if last page of paginated table
+ // doesn't fill all the vertical space due to fewer rows.
+ min-height: 305px;
+
+ &__header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ }
+
.table-container__header {
display: none;
}
@@ -8,17 +18,11 @@
.data-table__table {
thead {
.query_name__header {
- min-width: $col-lg;
+ min-width: $col-sm;
}
.last_fetched__header {
display: table-cell;
}
- @media (max-width: $break-md) {
- .last_fetched__header {
- display: none;
- width: 0;
- }
- }
}
tbody {
tr {
@@ -27,7 +31,7 @@
}
.query_name__cell {
- min-width: $col-lg;
+ min-width: $col-sm;
}
.last_fetched__cell {
.report-updated-cell {
@@ -49,19 +53,23 @@
opacity: 1;
}
}
- @media (max-width: $break-md) {
- .last_fetched__cell {
+
+ @media (min-width: $break-md) and (max-width: 1300px) {
+ .report-updated-cell__view-report--text {
display: none;
width: 0;
}
- .performance-cell__link-icon {
- display: inline-flex;
- align-self: center;
- width: initial;
+
+ td {
+ max-width: 140px;
}
}
}
}
}
}
+
+ .empty-header {
+ font-weight: $bold;
+ }
}
diff --git a/frontend/pages/hosts/details/cards/User/User.tsx b/frontend/pages/hosts/details/cards/User/User.tsx
index 6567fd17d6..7d662065fb 100644
--- a/frontend/pages/hosts/details/cards/User/User.tsx
+++ b/frontend/pages/hosts/details/cards/User/User.tsx
@@ -1,6 +1,5 @@
import React from "react";
import classnames from "classnames";
-import { noop } from "lodash";
import { IHostEndUser } from "interfaces/host";
diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
index 02f40aaa6f..360fcc32fc 100644
--- a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
+++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
@@ -250,19 +250,23 @@ const QueryDetailsPage = ({
isTeamMaintainerOrTeamAdmin;
// Function instead of constant eliminates race condition with filteredQueriesPath
- const backToQueriesPath = () => {
- return (
- filteredQueriesPath ||
- getPathWithQueryParams(PATHS.MANAGE_QUERIES, {
- team_id: currentTeamId,
- })
- );
+ const backPath = () => {
+ if (filteredQueriesPath) return filteredQueriesPath;
+
+ if (hostId) return getPathWithQueryParams(PATHS.HOST_DETAILS(hostId));
+
+ return getPathWithQueryParams(PATHS.MANAGE_QUERIES, {
+ team_id: currentTeamId,
+ });
};
return (
<>
-
+
{!isLoading && !isApiError && (
<>
@@ -329,6 +333,7 @@ const QueryDetailsPage = ({
router.push(
getPathWithQueryParams(PATHS.EDIT_QUERY(queryId), {
team_id: currentTeamId,
+ host_id: hostId,
})
);
}}
diff --git a/frontend/pages/queries/edit/EditQueryPage.tsx b/frontend/pages/queries/edit/EditQueryPage.tsx
index 0fe0dc39da..1a1ffdbc6c 100644
--- a/frontend/pages/queries/edit/EditQueryPage.tsx
+++ b/frontend/pages/queries/edit/EditQueryPage.tsx
@@ -176,6 +176,7 @@ const EditQueryPage = ({
router.push(
getPathWithQueryParams(location.pathname, {
team_id: storedQuery?.team_id?.toString(),
+ host_id: hostId,
})
);
}
@@ -266,6 +267,7 @@ const EditQueryPage = ({
router.push(
getPathWithQueryParams(PATHS.QUERY_DETAILS(query.id), {
team_id: query.team_id,
+ host_id: hostId,
})
);
renderFlash("success", "Query created!");
@@ -370,15 +372,36 @@ const EditQueryPage = ({
// Function instead of constant eliminates race condition
// Returns to queries details page, manage queries page with filters, or default manage queries page
- const backToQueriesPath = () =>
- queryId
- ? getPathWithQueryParams(PATHS.QUERY_DETAILS(queryId), {
- team_id: currentTeamId,
- })
- : filteredQueriesPath ||
- getPathWithQueryParams(PATHS.MANAGE_QUERIES, {
- team_id: currentTeamId,
- });
+ const backPath = () => {
+ if (queryId) {
+ return getPathWithQueryParams(PATHS.QUERY_DETAILS(queryId), {
+ team_id: currentTeamId,
+ host_id: hostId,
+ });
+ }
+
+ if (hostId) {
+ return getPathWithQueryParams(PATHS.HOST_DETAILS(hostId));
+ }
+
+ if (filteredQueriesPath) return filteredQueriesPath;
+
+ return getPathWithQueryParams(PATHS.MANAGE_QUERIES, {
+ team_id: currentTeamId,
+ });
+ };
+
+ const backButtonText = () => {
+ if (queryId) {
+ return "Back to report";
+ }
+
+ if (hostId) {
+ return "Back to host details";
+ }
+
+ return "Back to queries";
+ };
const showSidebar =
isSidebarOpen &&
@@ -394,10 +417,7 @@ const EditQueryPage = ({
<>
-
+
{
+ if (isFreeTier) return null;
+
+ if (currentTeamName) {
+ if (isEditing) {
+ return (
+
+ Editing query for {currentTeamName} team.
+
+ );
+ }
+ return (
+
+ Creating a new query for {currentTeamName} team.
+
+ );
+ }
+
+ if (isEditing) {
+ return Editing global query.
;
+ }
+ return Creating a new global query.
;
+ };
+
// Observers and observer+ of existing query
const renderNonEditableForm = (
+ {renderQueryTeam()}