mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Fleet UI: Query report (table, buttons, api calls, etc) (#14325)
## Issue Cerra #13472 ## Description - Surface query report on the `/queries/{id}` route - Include table buttons to show query and export query - Include results count - Clientside sorting and filtering for columns - Add mock data to frontend integration mocks and to API mocks for concurrent development - 331 + 351 + 2 = 684 lines of code is just mocking data and not actual changes - If modifying sorting/filter, modify the exported results sorting/filter as well - Last fetched column is sentence cased, sortable by chronological order and not alpha order of the readable string (e.g., "a year ago" should be sorted _after_ "over 1 month ago" if sorted most recent to oldest even though a comes before o in the alphabet) ## Screen recordings (Uses mock data) https://github.com/fleetdm/fleet/assets/71795832/22766f2b-3387-4a95-b505-b530dda582fa https://github.com/fleetdm/fleet/assets/71795832/5c2cd8cc-d00e-4ead-b111-e3b33cb7c955 # Checklist for submitter If some of the following don't apply, delete the relevant line. - TODO for QA: Added/updated E2E tests (consider testing some of the features mentioned in the description) - [x] Manual QA for all new/changed functionality
This commit is contained in:
parent
2adf3ce477
commit
a85f399cac
331
frontend/__mocks__/queryReportMock.ts
Normal file
331
frontend/__mocks__/queryReportMock.ts
Normal file
@ -0,0 +1,331 @@
|
||||
import { IQueryReport } from "interfaces/query_report";
|
||||
|
||||
const DEFAULT_QUERY_REPORT_MOCK: IQueryReport = {
|
||||
query_id: 31,
|
||||
results: [
|
||||
{
|
||||
host_id: 1,
|
||||
host_name: "foo",
|
||||
last_fetched: "2021-01-19T17:08:31Z",
|
||||
columns: {
|
||||
model: "Razer Viper",
|
||||
vendor: "Razer",
|
||||
model_id: "0078",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 1,
|
||||
host_name: "foo",
|
||||
last_fetched: "2021-01-19T17:08:31Z",
|
||||
columns: {
|
||||
model: "USB Keyboard",
|
||||
vendor: "VIA Labs, Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Keyboard",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "YubiKey OTP+FIDO+CCID",
|
||||
vendor: "Yubico",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Lenovo USB Optical Mouse",
|
||||
vendor: "PixArt",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Lenovo Traditional USB Keyboard",
|
||||
vendor: "Lenovo",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Bose",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB-C Digital AV Multiport Adapter",
|
||||
vendor: "Apple, Inc.",
|
||||
model_id: "1460",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB-C Digital AV Multiport Adapter",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "1460",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "Logitech Webcam C925e",
|
||||
model_id: "085b",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "Ambient Light Sensor",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "DELL Laser Mouse",
|
||||
model_id: "4d51",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "AppleUSBVHCIBCE Root Hub Simulation",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "QuickFire Rapid keyboard",
|
||||
vendor: "CM Storm",
|
||||
model_id: "0004",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "Lenovo USB Optical Mouse",
|
||||
vendor: "Lenovo",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "YubiKey FIDO+CCID",
|
||||
vendor: "Yubico",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 4,
|
||||
host_name: "car",
|
||||
last_fetched: "2023-01-14T12:40:30Z",
|
||||
columns: {
|
||||
model: "USB2.0 Hub",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "FaceTime HD Camera (Display)",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "1112",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Apple Internal Keyboard / Trackpad",
|
||||
model_id: "027e",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Apple Thunderbolt Display",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "9227",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "AppleUSBXHCI Root Hub Simulation",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "8007",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Apple T2 Controller",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "8233",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "4-Port USB 2.0 Hub",
|
||||
vendor: "Generic",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "USB 10_100_1000 LAN",
|
||||
vendor: "Realtek",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "USB Mouse",
|
||||
vendor: "Razor",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "USB Audio",
|
||||
vendor: "Apple, Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 6,
|
||||
host_name: "moo",
|
||||
last_fetched: "2023-09-20T07:02:34Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 6,
|
||||
host_name: "moo",
|
||||
last_fetched: "2023-09-20T07:02:34Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 6,
|
||||
host_name: "moo",
|
||||
last_fetched: "2023-09-20T07:02:34Z",
|
||||
columns: {
|
||||
model: "LG Monitor Controls",
|
||||
vendor: "LG Electronics Inc.",
|
||||
model_id: "9a39",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const createMockQueryReport = (
|
||||
overrides?: Partial<IQueryReport>
|
||||
): IQueryReport => {
|
||||
return { ...DEFAULT_QUERY_REPORT_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
export default createMockQueryReport;
|
12
frontend/interfaces/query_report.ts
Normal file
12
frontend/interfaces/query_report.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface IQueryReportResultRow {
|
||||
host_id: number;
|
||||
host_name: string;
|
||||
last_fetched: string;
|
||||
columns: any;
|
||||
}
|
||||
|
||||
// Query report
|
||||
export interface IQueryReport {
|
||||
query_id: number;
|
||||
results: IQueryReportResultRow[];
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext } from "react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { InjectedRouter, Params } from "react-router/lib/Router";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
@ -12,8 +12,10 @@ import {
|
||||
IGetQueryResponse,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import { IQueryReport } from "interfaces/query_report";
|
||||
|
||||
import queryAPI from "services/entities/queries";
|
||||
import queryReportAPI, { ISortOption } from "services/entities/query_report";
|
||||
|
||||
import Spinner from "components/Spinner/Spinner";
|
||||
import Button from "components/buttons/Button";
|
||||
@ -23,15 +25,20 @@ import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper";
|
||||
import QueryAutomationsStatusIndicator from "pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator";
|
||||
import DataError from "components/DataError/DataError";
|
||||
import LogDestinationIndicator from "components/LogDestinationIndicator/LogDestinationIndicator";
|
||||
import CachedDetails from "../components/CachedDetails/CachedDetails";
|
||||
import QueryReport from "../components/QueryReport/QueryReport";
|
||||
import NoResults from "../components/NoResults/NoResults";
|
||||
|
||||
import {
|
||||
DEFAULT_SORT_HEADER,
|
||||
DEFAULT_SORT_DIRECTION,
|
||||
} from "./QueryDetailsPageConfig";
|
||||
|
||||
interface IQueryDetailsPageProps {
|
||||
router: InjectedRouter; // v3
|
||||
params: Params;
|
||||
location: {
|
||||
pathname: string;
|
||||
query: { team_id?: string };
|
||||
query: { team_id?: string; order_key?: string; order_direction?: string };
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
@ -43,7 +50,20 @@ const QueryDetailsPage = ({
|
||||
params: { id: paramsQueryId },
|
||||
location,
|
||||
}: IQueryDetailsPageProps): JSX.Element => {
|
||||
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
|
||||
const queryId = parseInt(paramsQueryId, 10);
|
||||
const queryParams = location.query;
|
||||
|
||||
// Functions to avoid race conditions
|
||||
const initialSortBy: ISortOption[] = (() => {
|
||||
return [
|
||||
{
|
||||
key: queryParams?.order_key ?? DEFAULT_SORT_HEADER,
|
||||
direction: queryParams?.order_direction ?? DEFAULT_SORT_DIRECTION,
|
||||
},
|
||||
];
|
||||
})();
|
||||
|
||||
const [sortBy, setSortBy] = useState<ISortOption[]>(initialSortBy);
|
||||
|
||||
const {
|
||||
currentTeamName: teamNameForQuery,
|
||||
@ -91,7 +111,7 @@ const QueryDetailsPage = ({
|
||||
error: storedQueryError,
|
||||
} = useQuery<IGetQueryResponse, Error, ISchedulableQuery>(
|
||||
["query", queryId],
|
||||
() => queryAPI.load(queryId as number),
|
||||
() => queryAPI.load(queryId),
|
||||
{
|
||||
enabled: !!queryId,
|
||||
refetchOnWindowFocus: false,
|
||||
@ -111,8 +131,26 @@ const QueryDetailsPage = ({
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = isStoredQueryLoading; // TODO: Add || isCachedResultsLoading for new API response
|
||||
const isApiError = storedQueryError || false; // TODO: Add || isCachedResultsError for new API response
|
||||
const {
|
||||
isLoading: isQueryReportLoading,
|
||||
data: queryReport,
|
||||
error: queryReportError,
|
||||
} = useQuery<IQueryReport, Error, IQueryReport>(
|
||||
[],
|
||||
() =>
|
||||
queryReportAPI.load({
|
||||
sortBy,
|
||||
id: queryId,
|
||||
}),
|
||||
{
|
||||
enabled: !!queryId,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: (error) => handlePageError(error),
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = isStoredQueryLoading || isQueryReportLoading;
|
||||
const isApiError = storedQueryError || queryReportError;
|
||||
|
||||
const renderHeader = () => {
|
||||
const canEditQuery =
|
||||
@ -172,7 +210,9 @@ const QueryDetailsPage = ({
|
||||
{!isLoading && !isApiError && (
|
||||
<div className={`${baseClass}__settings`}>
|
||||
<div className={`${baseClass}__automations`}>
|
||||
<TooltipWrapper tipContent="Query automations let you send data to your log destination on a schedule. When automations are on, data is sent according to a query’s frequency.">
|
||||
<TooltipWrapper
|
||||
tipContent={`Query automations let you send data to your log destination on a schedule. When automations are <b>on</b>, data is sent according to a query’s frequency.`}
|
||||
>
|
||||
Automations:
|
||||
</TooltipWrapper>
|
||||
<QueryAutomationsStatusIndicator
|
||||
@ -198,8 +238,7 @@ const QueryDetailsPage = ({
|
||||
const loggingSnapshot = storedQuery?.logging === "snapshot";
|
||||
const disabledCaching =
|
||||
disabledCachingGlobally || discardDataEnabled || !loggingSnapshot;
|
||||
const emptyCache = true; // TODO: Update with API response
|
||||
const errorsOnly = true; // TODO: Update with API response
|
||||
const emptyCache = queryReport?.results.length === 0; // TODO: Update with API response
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
@ -221,11 +260,10 @@ const QueryDetailsPage = ({
|
||||
disabledCachingGlobally={disabledCachingGlobally}
|
||||
discardDataEnabled={discardDataEnabled}
|
||||
loggingSnapshot={loggingSnapshot}
|
||||
errorsOnly={errorsOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <CachedDetails />; // TODO: Everything related to new APIs including surfacing errorsOnly
|
||||
return <QueryReport queryReport={queryReport} />; // TODO: Everything related to new APIs including surfacing errorsOnly
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -0,0 +1,13 @@
|
||||
// TODO
|
||||
export const QUERY_DETAILS_PAGE_FILTER_KEYS = ["model", "vendor"] as const;
|
||||
|
||||
// TODO: refactor to use this type as the location.query prop of the page
|
||||
export type QueryDetailsPageQueryParams = Record<
|
||||
| "order_key"
|
||||
| "order_direction"
|
||||
| typeof QUERY_DETAILS_PAGE_FILTER_KEYS[number],
|
||||
string
|
||||
>;
|
||||
|
||||
export const DEFAULT_SORT_HEADER = "host_name";
|
||||
export const DEFAULT_SORT_DIRECTION = "asc";
|
@ -39,6 +39,10 @@
|
||||
&__log-destination {
|
||||
display: flex;
|
||||
gap: $pad-small;
|
||||
|
||||
.component__tooltip-wrapper__element {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-table__inner {
|
||||
|
@ -1,14 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// TODO: This whole section
|
||||
// interface ICachedDetailsProps {
|
||||
//
|
||||
// }
|
||||
|
||||
const baseClass = "cached-details";
|
||||
|
||||
const CachedDetails = (): JSX.Element => {
|
||||
return <div className={`${baseClass}__wrapper`}>TODO</div>;
|
||||
};
|
||||
|
||||
export default CachedDetails;
|
@ -1 +0,0 @@
|
||||
export { default } from "./CachedDetails";
|
@ -14,7 +14,6 @@ interface INoResultsProps {
|
||||
disabledCachingGlobally: boolean;
|
||||
discardDataEnabled: boolean;
|
||||
loggingSnapshot: boolean;
|
||||
errorsOnly: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "no-results";
|
||||
@ -26,7 +25,6 @@ const NoResults = ({
|
||||
disabledCachingGlobally,
|
||||
discardDataEnabled,
|
||||
loggingSnapshot,
|
||||
errorsOnly,
|
||||
}: INoResultsProps): JSX.Element => {
|
||||
// Returns how many seconds it takes to expect a cached update
|
||||
const secondsCheckbackTime = () => {
|
||||
@ -92,14 +90,15 @@ const NoResults = ({
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (errorsOnly) {
|
||||
return (
|
||||
<>
|
||||
This query had trouble collecting data on some hosts. Check out the{" "}
|
||||
<strong>Errors</strong> tab to see why.
|
||||
</>
|
||||
);
|
||||
}
|
||||
// No errors will be reported in V1
|
||||
// if (errorsOnly) {
|
||||
// return (
|
||||
// <>
|
||||
// This query had trouble collecting data on some hosts. Check out the{" "}
|
||||
// <strong>Errors</strong> tab to see why.
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
return "This query has returned no data so far.";
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,142 @@
|
||||
import React, { useState, useContext, useEffect } from "react";
|
||||
|
||||
import { Row, Column } from "react-table";
|
||||
import FileSaver from "file-saver";
|
||||
import { QueryContext } from "context/query";
|
||||
|
||||
import {
|
||||
generateCSVFilename,
|
||||
generateCSVQueryResults,
|
||||
} from "utilities/generate_csv";
|
||||
import { IQueryReport, IQueryReportResultRow } from "interfaces/query_report";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import ShowQueryModal from "components/modals/ShowQueryModal";
|
||||
|
||||
import generateResultsTableHeaders from "./QueryReportTableConfig";
|
||||
|
||||
interface IQueryReportProps {
|
||||
queryReport?: IQueryReport;
|
||||
}
|
||||
|
||||
const baseClass = "query-report";
|
||||
const CSV_TITLE = "Query";
|
||||
|
||||
const tableResults = (results: IQueryReportResultRow[]) => {
|
||||
return results.map((result: IQueryReportResultRow) => {
|
||||
const hostInfoColumns = {
|
||||
host_display_name: result.host_name,
|
||||
last_fetched: result.last_fetched,
|
||||
};
|
||||
|
||||
// hostInfoColumns displays the host metadata that is returned with every query
|
||||
// result.columns are the variable columns returned by the API that differ per query
|
||||
return { ...hostInfoColumns, ...result.columns };
|
||||
});
|
||||
};
|
||||
|
||||
const QueryReport = ({ queryReport }: IQueryReportProps): JSX.Element => {
|
||||
const { lastEditedQueryName, lastEditedQueryBody } = useContext(QueryContext);
|
||||
|
||||
const [showQueryModal, setShowQueryModal] = useState(false);
|
||||
const [filteredResults, setFilteredResults] = useState<Row[]>(
|
||||
tableResults(queryReport?.results || [])
|
||||
);
|
||||
const [tableHeaders, setTableHeaders] = useState<Column[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryReport && queryReport.results && queryReport.results.length > 0) {
|
||||
const generatedTableHeaders = generateResultsTableHeaders(
|
||||
tableResults(queryReport.results)
|
||||
);
|
||||
// Update tableHeaders if new headers are found
|
||||
if (generatedTableHeaders !== tableHeaders) {
|
||||
setTableHeaders(generatedTableHeaders);
|
||||
}
|
||||
}
|
||||
}, [queryReport]); // Cannot use tableHeaders as it will cause infinite loop with setTableHeaders
|
||||
|
||||
const onExportQueryResults = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
evt.preventDefault();
|
||||
FileSaver.saveAs(
|
||||
generateCSVQueryResults(
|
||||
filteredResults,
|
||||
generateCSVFilename(
|
||||
`${lastEditedQueryName || CSV_TITLE} - Query Report`
|
||||
),
|
||||
tableHeaders
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const onShowQueryModal = () => {
|
||||
setShowQueryModal(!showQueryModal);
|
||||
};
|
||||
|
||||
const renderNoResults = () => {
|
||||
return <p className="no-results-message">TODO</p>;
|
||||
};
|
||||
|
||||
const renderTableButtons = () => {
|
||||
return (
|
||||
<div className={`${baseClass}__results-cta`}>
|
||||
<Button
|
||||
className={`${baseClass}__show-query-btn`}
|
||||
onClick={onShowQueryModal}
|
||||
variant="text-icon"
|
||||
>
|
||||
<>
|
||||
Show query <Icon name="eye" />
|
||||
</>
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__export-btn`}
|
||||
onClick={onExportQueryResults}
|
||||
variant="text-icon"
|
||||
>
|
||||
<>
|
||||
Export results
|
||||
<Icon name="download" color="core-fleet-blue" />
|
||||
</>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTable = () => {
|
||||
return (
|
||||
<div className={`${baseClass}__results-table-container`}>
|
||||
<TableContainer
|
||||
columns={tableHeaders}
|
||||
data={tableResults(queryReport?.results || [])}
|
||||
emptyComponent={renderNoResults}
|
||||
isLoading={false}
|
||||
isClientSidePagination
|
||||
isClientSideFilter
|
||||
isMultiColumnFilter
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
resultsTitle="results"
|
||||
customControl={() => renderTableButtons()}
|
||||
setExportRows={setFilteredResults}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
{renderTable()}
|
||||
{showQueryModal && (
|
||||
<ShowQueryModal
|
||||
query={lastEditedQueryBody}
|
||||
onCancel={onShowQueryModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryReport;
|
@ -0,0 +1,93 @@
|
||||
/* 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 {
|
||||
CellProps,
|
||||
Column,
|
||||
ColumnInstance,
|
||||
ColumnInterface,
|
||||
HeaderProps,
|
||||
TableInstance,
|
||||
} from "react-table";
|
||||
|
||||
import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColumnFilter";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||
|
||||
import { humanHostLastSeen } from "utilities/helpers";
|
||||
|
||||
type IHeaderProps = HeaderProps<TableInstance> & {
|
||||
column: ColumnInstance & IDataColumn;
|
||||
};
|
||||
|
||||
type ICellProps = CellProps<TableInstance>;
|
||||
|
||||
interface IDataColumn extends ColumnInterface {
|
||||
title?: string;
|
||||
accessor: string;
|
||||
}
|
||||
|
||||
const _unshiftHostname = (headers: IDataColumn[]) => {
|
||||
const newHeaders = [...headers];
|
||||
const displayNameIndex = headers.findIndex(
|
||||
(h) => h.id === "host_display_name"
|
||||
);
|
||||
if (displayNameIndex >= 0) {
|
||||
// remove hostname header from headers
|
||||
const [displayNameHeader] = newHeaders.splice(displayNameIndex, 1);
|
||||
// reformat title and insert at start of headers array
|
||||
newHeaders.unshift({ ...displayNameHeader, title: "Host" });
|
||||
}
|
||||
// TODO: Remove after v5 when host_hostname is removed rom API response.
|
||||
const hostNameIndex = headers.findIndex((h) => h.id === "host_hostname");
|
||||
if (hostNameIndex >= 0) {
|
||||
newHeaders.splice(hostNameIndex, 1);
|
||||
}
|
||||
// end remove
|
||||
return newHeaders;
|
||||
};
|
||||
|
||||
const generateResultsTableHeaders = (results: any[]): Column[] => {
|
||||
/* Results include an array of objects, each representing a table row
|
||||
Each key value pair in an object represents a column name and value
|
||||
To create headers, use JS set to create an array of all unique column names */
|
||||
const uniqueColumnNames = Array.from(
|
||||
results.reduce(
|
||||
(s, o) => Object.keys(o).reduce((t, k) => t.add(k), s),
|
||||
new Set() // Set prevents listing duplicate headers
|
||||
)
|
||||
);
|
||||
|
||||
const headers = uniqueColumnNames.map((key) => {
|
||||
return {
|
||||
id: key as string,
|
||||
title: key as string,
|
||||
Header: (headerProps: IHeaderProps) => (
|
||||
<HeaderCell
|
||||
value={
|
||||
// Sentence case last fetched
|
||||
headerProps.column.title === "last_fetched"
|
||||
? "Last fetched"
|
||||
: headerProps.column.title || headerProps.column.id
|
||||
}
|
||||
isSortedDesc={headerProps.column.isSortedDesc}
|
||||
/>
|
||||
),
|
||||
accessor: key as string,
|
||||
Cell: (cellProps: ICellProps) => {
|
||||
// Filters chronologically by date, but UI displays readable last fetched
|
||||
if (cellProps.column.id === "last_fetched") {
|
||||
return humanHostLastSeen(cellProps?.cell?.value);
|
||||
}
|
||||
return cellProps?.cell?.value || null;
|
||||
},
|
||||
Filter: DefaultColumnFilter,
|
||||
filterType: "text",
|
||||
disableSortBy: false,
|
||||
};
|
||||
});
|
||||
return _unshiftHostname(headers);
|
||||
};
|
||||
|
||||
export default generateResultsTableHeaders;
|
@ -0,0 +1,14 @@
|
||||
.query-report {
|
||||
&__wrapper {
|
||||
margin-top: $pad-large;
|
||||
|
||||
.host_id__header {
|
||||
width: 95px; // Min width for 6 digits host IDs
|
||||
}
|
||||
}
|
||||
|
||||
&__results-cta {
|
||||
display: flex;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./QueryReport";
|
50
frontend/services/entities/query_report.ts
Normal file
50
frontend/services/entities/query_report.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
// import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
// Mock API requests to be used in developing FE for #7766 in parallel with BE development
|
||||
import { sendRequest } from "services/mock_service/service/service";
|
||||
|
||||
export interface ISortOption {
|
||||
key: string;
|
||||
direction: string;
|
||||
}
|
||||
|
||||
export interface ILoadQueryReportOptions {
|
||||
id: number;
|
||||
sortBy: ISortOption[];
|
||||
}
|
||||
|
||||
const getSortParams = (sortOptions?: ISortOption[]) => {
|
||||
if (sortOptions === undefined || sortOptions.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const sortItem = sortOptions[0];
|
||||
return {
|
||||
order_key: sortItem.key,
|
||||
order_direction: sortItem.direction,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
load: ({ id, sortBy }: ILoadQueryReportOptions) => {
|
||||
const sortParams = getSortParams(sortBy);
|
||||
|
||||
const { QUERIES } = endpoints;
|
||||
|
||||
const queryParams = {
|
||||
order_key: sortParams.order_key,
|
||||
order_direction: sortParams.order_direction,
|
||||
};
|
||||
|
||||
const queryString = buildQueryStringFromParams(queryParams);
|
||||
|
||||
// const endpoint = `${QUERIES}/${id}/report`;
|
||||
const endpoint = `${QUERIES}/113/report`;
|
||||
const path = `${endpoint}?${queryString}`;
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
};
|
@ -33,6 +33,8 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = {
|
||||
"queries/7": RESPONSES.globalQuery6,
|
||||
"queries/8": RESPONSES.teamQuery2,
|
||||
"queries?team_id=13": RESPONSES.teamQueries,
|
||||
"queries/113/report?order_key=host_name&order_direction=asc":
|
||||
RESPONSES.queryReport,
|
||||
},
|
||||
POST: {
|
||||
// request body is ISelectedTargets
|
||||
|
@ -598,6 +598,356 @@ const teamQueries = {
|
||||
],
|
||||
};
|
||||
|
||||
const queryReport = {
|
||||
query_id: 31,
|
||||
results: [
|
||||
{
|
||||
host_id: 1,
|
||||
host_name: "foo",
|
||||
last_fetched: "2021-01-19T17:08:31Z",
|
||||
columns: {
|
||||
model: "Razer Viper",
|
||||
vendor: "Razer",
|
||||
model_id: "0078",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 1,
|
||||
host_name: "foo",
|
||||
last_fetched: "2021-01-19T17:08:31Z",
|
||||
columns: {
|
||||
model: "USB Keyboard",
|
||||
vendor: "VIA Labs, Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Keyboard",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "YubiKey OTP+FIDO+CCID",
|
||||
vendor: "Yubico",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Lenovo USB Optical Mouse",
|
||||
vendor: "PixArt",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Lenovo Traditional USB Keyboard",
|
||||
vendor: "Lenovo",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Bose",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB-C Digital AV Multiport Adapter",
|
||||
vendor: "Apple, Inc.",
|
||||
model_id: "1460",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB-C Digital AV Multiport Adapter",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "1460",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "Logitech Webcam C925e",
|
||||
model_id: "085b",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "Ambient Light Sensor",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "DELL Laser Mouse",
|
||||
model_id: "4d51",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "AppleUSBVHCIBCE Root Hub Simulation",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "QuickFire Rapid keyboard",
|
||||
vendor: "CM Storm",
|
||||
model_id: "0004",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "Lenovo USB Optical Mouse",
|
||||
vendor: "Lenovo",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "YubiKey FIDO+CCID",
|
||||
vendor: "Yubico",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 4,
|
||||
host_name: "car",
|
||||
last_fetched: "2023-01-14T12:40:30Z",
|
||||
columns: {
|
||||
model: "USB2.0 Hub",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "FaceTime HD Camera (Display)",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "1112",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Apple Internal Keyboard / Trackpad",
|
||||
model_id: "027e",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Apple Thunderbolt Display",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "9227",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "AppleUSBXHCI Root Hub Simulation",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "8007",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Apple T2 Controller",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "8233",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "4-Port USB 2.0 Hub",
|
||||
vendor: "Generic",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "USB 10_100_1000 LAN",
|
||||
vendor: "Realtek",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "USB Mouse",
|
||||
vendor: "Razor",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "USB Audio",
|
||||
vendor: "Apple, Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 6,
|
||||
host_name: "moo",
|
||||
last_fetched: "2023-09-20T07:02:34Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 6,
|
||||
host_name: "moo",
|
||||
last_fetched: "2023-09-20T07:02:34Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 6,
|
||||
host_name: "moo",
|
||||
last_fetched: "2023-09-20T07:02:34Z",
|
||||
columns: {
|
||||
model: "LG Monitor Controls",
|
||||
vendor: "LG Electronics Inc.",
|
||||
model_id: "9a39",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 9,
|
||||
host_name: "moo moo",
|
||||
last_fetched: "2023-09-28T02:02:34Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 9,
|
||||
host_name: "moo moo",
|
||||
last_fetched: "2023-09-28T02:02:34Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 9,
|
||||
host_name: "moo moo",
|
||||
last_fetched: "2023-09-28T02:02:34Z",
|
||||
columns: {
|
||||
model: "LG Monitor Controls",
|
||||
vendor: "LG Electronics Inc.",
|
||||
model_id: "9a39",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const globalQuery1 = { query: globalQueries.queries[0] };
|
||||
const globalQuery2 = { query: globalQueries.queries[1] };
|
||||
const globalQuery3 = { query: globalQueries.queries[2] };
|
||||
@ -611,6 +961,7 @@ export default {
|
||||
count,
|
||||
hosts,
|
||||
labels,
|
||||
queryReport,
|
||||
globalQueries,
|
||||
globalQuery1,
|
||||
globalQuery2,
|
||||
|
@ -14,7 +14,7 @@ export const generateCSVFilename = (descriptor: string) => {
|
||||
return `${descriptor} (${format(new Date(), "MM-dd-yy hh-mm-ss")}).csv`;
|
||||
};
|
||||
|
||||
// Query results and query errors
|
||||
// Live query results, live query errors, and query report
|
||||
export const generateCSVQueryResults = (
|
||||
rows: Row[],
|
||||
filename: string,
|
||||
@ -35,7 +35,7 @@ export const generateCSVQueryResults = (
|
||||
);
|
||||
};
|
||||
|
||||
// Policy results only
|
||||
// Live policy results only
|
||||
export const generateCSVPolicyResults = (
|
||||
rows: { host: string; status: string }[],
|
||||
filename: string
|
||||
@ -45,7 +45,7 @@ export const generateCSVPolicyResults = (
|
||||
});
|
||||
};
|
||||
|
||||
// Policy errors only
|
||||
// Live policy errors only
|
||||
export const generateCSVPolicyErrors = (
|
||||
rows: ICampaignError[],
|
||||
filename: string
|
||||
|
Loading…
Reference in New Issue
Block a user