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:
RachelElysia 2023-10-09 13:38:34 -07:00 committed by GitHub
parent 2adf3ce477
commit a85f399cac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1075 additions and 40 deletions

View 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;

View 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[];
}

View File

@ -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 querys 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 querys 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 (

View File

@ -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";

View File

@ -39,6 +39,10 @@
&__log-destination {
display: flex;
gap: $pad-small;
.component__tooltip-wrapper__element {
font-weight: $bold;
}
}
.empty-table__inner {

View File

@ -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;

View File

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

View File

@ -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.";
};

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

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

View 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);
},
};

View File

@ -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

View File

@ -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,

View File

@ -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