mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Add pagination and multi-column sort to live query results table UI (#3647)
* Refactor live query results to react-table
This commit is contained in:
parent
51cd0ff148
commit
39b7c7d9f9
BIN
assets/images/icon-filter-grey-12x12@2x.png
Normal file
BIN
assets/images/icon-filter-grey-12x12@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 348 B |
1
changes/issue-2714-live-query-table
Normal file
1
changes/issue-2714-live-query-table
Normal file
@ -0,0 +1 @@
|
|||||||
|
* Add client-side pagination and multi-column filter to live query results table
|
@ -24,6 +24,8 @@ describe("DataTable - component", () => {
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={data}
|
data={data}
|
||||||
|
sortHeader={"name"}
|
||||||
|
sortDirection={"desc"}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
onSort={noop}
|
onSort={noop}
|
||||||
showMarkAllPages={false}
|
showMarkAllPages={false}
|
||||||
@ -31,6 +33,7 @@ describe("DataTable - component", () => {
|
|||||||
resultsTitle="users"
|
resultsTitle="users"
|
||||||
defaultPageSize={DEFAULT_PAGE_SIZE}
|
defaultPageSize={DEFAULT_PAGE_SIZE}
|
||||||
disableMultiRowSelect={false}
|
disableMultiRowSelect={false}
|
||||||
|
onPrimarySelectActionClick={noop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -73,6 +76,7 @@ describe("DataTable - component", () => {
|
|||||||
resultsTitle="users"
|
resultsTitle="users"
|
||||||
defaultPageSize={DEFAULT_PAGE_SIZE}
|
defaultPageSize={DEFAULT_PAGE_SIZE}
|
||||||
disableMultiRowSelect={false}
|
disableMultiRowSelect={false}
|
||||||
|
onPrimarySelectActionClick={noop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -99,6 +103,7 @@ describe("DataTable - component", () => {
|
|||||||
resultsTitle="users"
|
resultsTitle="users"
|
||||||
defaultPageSize={DEFAULT_PAGE_SIZE}
|
defaultPageSize={DEFAULT_PAGE_SIZE}
|
||||||
disableMultiRowSelect={false}
|
disableMultiRowSelect={false}
|
||||||
|
onPrimarySelectActionClick={noop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -133,6 +138,7 @@ describe("DataTable - component", () => {
|
|||||||
resultsTitle="users"
|
resultsTitle="users"
|
||||||
defaultPageSize={DEFAULT_PAGE_SIZE}
|
defaultPageSize={DEFAULT_PAGE_SIZE}
|
||||||
disableMultiRowSelect={false}
|
disableMultiRowSelect={false}
|
||||||
|
onPrimarySelectActionClick={noop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -156,6 +162,7 @@ describe("DataTable - component", () => {
|
|||||||
resultsTitle="users"
|
resultsTitle="users"
|
||||||
defaultPageSize={DEFAULT_PAGE_SIZE}
|
defaultPageSize={DEFAULT_PAGE_SIZE}
|
||||||
disableMultiRowSelect={false}
|
disableMultiRowSelect={false}
|
||||||
|
onPrimarySelectActionClick={noop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
/* 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, { useMemo, useEffect, useCallback, useContext } from "react";
|
import React, { useMemo, useEffect, useCallback, useContext } from "react";
|
||||||
import { TableContext } from "context/table";
|
import { TableContext } from "context/table";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import {
|
import {
|
||||||
useTable,
|
useTable,
|
||||||
@ -9,6 +11,8 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
usePagination,
|
usePagination,
|
||||||
useFilters,
|
useFilters,
|
||||||
|
HeaderGroup,
|
||||||
|
Column,
|
||||||
} from "react-table";
|
} from "react-table";
|
||||||
import { isString, kebabCase, noop } from "lodash";
|
import { isString, kebabCase, noop } from "lodash";
|
||||||
import { useDebouncedCallback } from "use-debounce/lib";
|
import { useDebouncedCallback } from "use-debounce/lib";
|
||||||
@ -28,7 +32,7 @@ import ActionButton, { IActionButtonProps } from "./ActionButton";
|
|||||||
const baseClass = "data-table-container";
|
const baseClass = "data-table-container";
|
||||||
|
|
||||||
interface IDataTableProps {
|
interface IDataTableProps {
|
||||||
columns: any;
|
columns: Column[];
|
||||||
data: any;
|
data: any;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
manualSortBy?: boolean;
|
manualSortBy?: boolean;
|
||||||
@ -256,6 +260,15 @@ const DataTable = ({
|
|||||||
[disableMultiRowSelect, onSelectSingleRow, toggleAllRowsSelected]
|
[disableMultiRowSelect, onSelectSingleRow, toggleAllRowsSelected]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderColumnHeader = (column: HeaderGroup) => {
|
||||||
|
return (
|
||||||
|
<div className="column-header">
|
||||||
|
{column.render("Header")}
|
||||||
|
{column.Filter && column.render("Filter")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderSelectedCount = (): JSX.Element => {
|
const renderSelectedCount = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
@ -422,7 +435,7 @@ const DataTable = ({
|
|||||||
className={column.id ? `${column.id}__header` : ""}
|
className={column.id ? `${column.id}__header` : ""}
|
||||||
{...column.getHeaderProps(column.getSortByToggleProps())}
|
{...column.getHeaderProps(column.getSortByToggleProps())}
|
||||||
>
|
>
|
||||||
<div>{column.render("Header")}</div>
|
{renderColumnHeader(column)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@ -486,15 +499,4 @@ const DataTable = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DataTable.propTypes = {
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object), // TODO: create proper interface for this
|
|
||||||
data: PropTypes.arrayOf(PropTypes.object), // TODO: create proper interface for this
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
sortHeader: PropTypes.string,
|
|
||||||
sortDirection: PropTypes.string,
|
|
||||||
onSort: PropTypes.func,
|
|
||||||
onPrimarySelectActionClick: PropTypes.func,
|
|
||||||
secondarySelectActions: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DataTable;
|
export default DataTable;
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FilterProps, TableInstance } from "react-table";
|
||||||
|
|
||||||
|
import SearchField from "components/forms/fields/SearchField";
|
||||||
|
|
||||||
|
const DefaultColumnFilter = ({
|
||||||
|
column,
|
||||||
|
}: FilterProps<TableInstance>): JSX.Element => {
|
||||||
|
const { setFilter } = column;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"filter-cell"}>
|
||||||
|
<SearchField
|
||||||
|
placeholder=""
|
||||||
|
onChange={(searchString) => {
|
||||||
|
setFilter(searchString || undefined); // Set undefined to remove the filter entirely
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DefaultColumnFilter;
|
@ -0,0 +1,27 @@
|
|||||||
|
.filter-cell {
|
||||||
|
input {
|
||||||
|
height: 28px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: $x-small;
|
||||||
|
background-color: $ui-off-white;
|
||||||
|
border: 1px solid $ui-fleet-blue-15;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin-top: $pad-xsmall;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field__input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
color: $core-fleet-blue;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
content: url(../assets/images/icon-filter-grey-12x12@2x.png);
|
||||||
|
transform: scale(0.5);
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./DefaultColumnFilter";
|
@ -3,9 +3,14 @@ import React from "react";
|
|||||||
interface IHeaderCellProps {
|
interface IHeaderCellProps {
|
||||||
value: string;
|
value: string;
|
||||||
isSortedDesc?: boolean;
|
isSortedDesc?: boolean;
|
||||||
|
disableSortBy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderCell = ({ value, isSortedDesc }: IHeaderCellProps): JSX.Element => {
|
const HeaderCell = ({
|
||||||
|
value,
|
||||||
|
isSortedDesc,
|
||||||
|
disableSortBy,
|
||||||
|
}: IHeaderCellProps): JSX.Element => {
|
||||||
let sortArrowClass = "";
|
let sortArrowClass = "";
|
||||||
if (isSortedDesc === undefined) {
|
if (isSortedDesc === undefined) {
|
||||||
sortArrowClass = "";
|
sortArrowClass = "";
|
||||||
@ -18,10 +23,12 @@ const HeaderCell = ({ value, isSortedDesc }: IHeaderCellProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div className={`header-cell ${sortArrowClass}`}>
|
<div className={`header-cell ${sortArrowClass}`}>
|
||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
<div className="sort-arrows">
|
{!disableSortBy && (
|
||||||
<span className="ascending-arrow" />
|
<div className="sort-arrows">
|
||||||
<span className="descending-arrow" />
|
<span className="ascending-arrow" />
|
||||||
</div>
|
<span className="descending-arrow" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,4 +4,7 @@ export type IDataColumn = Column & {
|
|||||||
title?: string;
|
title?: string;
|
||||||
disableHidden?: boolean;
|
disableHidden?: boolean;
|
||||||
disableSortBy?: boolean;
|
disableSortBy?: boolean;
|
||||||
|
filterValue?: any;
|
||||||
|
preFilteredRows?: any;
|
||||||
|
setFilter?: any;
|
||||||
};
|
};
|
||||||
|
@ -3,22 +3,22 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from "file-saver";
|
||||||
import { filter, get, keys, omit } from "lodash";
|
import { filter, get } from "lodash";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import convertToCSV from "utilities/convert_to_csv"; // @ts-ignore
|
import convertToCSV from "utilities/convert_to_csv"; // @ts-ignore
|
||||||
import filterArrayByHash from "utilities/filter_array_by_hash";
|
|
||||||
import { ICampaign, ICampaignQueryResult } from "interfaces/campaign";
|
import { ICampaign, ICampaignQueryResult } from "interfaces/campaign";
|
||||||
import { ITarget } from "interfaces/target";
|
import { ITarget } from "interfaces/target";
|
||||||
|
|
||||||
import Button from "components/buttons/Button"; // @ts-ignore
|
import Button from "components/buttons/Button"; // @ts-ignore
|
||||||
import FleetIcon from "components/icons/FleetIcon"; // @ts-ignore
|
|
||||||
import InputField from "components/forms/fields/InputField";
|
|
||||||
import QueryResultsRow from "components/queries/QueryResultsRow";
|
|
||||||
import Spinner from "components/Spinner";
|
import Spinner from "components/Spinner";
|
||||||
|
import TableContainer from "components/TableContainer";
|
||||||
import TabsWrapper from "components/TabsWrapper";
|
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";
|
||||||
|
|
||||||
|
import resultsTableHeaders from "./QueryResultsTableConfig";
|
||||||
|
|
||||||
interface IQueryResultsProps {
|
interface IQueryResultsProps {
|
||||||
campaign: ICampaign;
|
campaign: ICampaign;
|
||||||
isQueryFinished: boolean;
|
isQueryFinished: boolean;
|
||||||
@ -61,10 +61,7 @@ const QueryResults = ({
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
const [pageTitle, setPageTitle] = useState<string>(PAGE_TITLES.RUNNING);
|
const [pageTitle, setPageTitle] = useState<string>(PAGE_TITLES.RUNNING);
|
||||||
const [resultsFilter, setResultsFilter] = useState<{ [key: string]: string }>(
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const [activeColumn, setActiveColumn] = useState<string>("");
|
|
||||||
const [navTabIndex, setNavTabIndex] = useState(0);
|
const [navTabIndex, setNavTabIndex] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -75,17 +72,6 @@ const QueryResults = ({
|
|||||||
}
|
}
|
||||||
}, [isQueryFinished]);
|
}, [isQueryFinished]);
|
||||||
|
|
||||||
const onFilterAttribute = (attribute: string) => {
|
|
||||||
return (value: string) => {
|
|
||||||
setResultsFilter({
|
|
||||||
...resultsFilter,
|
|
||||||
[attribute]: value,
|
|
||||||
});
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const onExportQueryResults = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
const onExportQueryResults = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
@ -133,60 +119,36 @@ const QueryResults = ({
|
|||||||
goToQueryEditor();
|
goToQueryEditor();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTableHeaderColumn = (column: string, index: number) => {
|
const renderNoResults = () => {
|
||||||
const filterable = column === "hostname" ? "host_hostname" : column;
|
|
||||||
const filterIconClassName = classnames(`${baseClass}__filter-icon`, {
|
|
||||||
[`${baseClass}__filter-icon--is-active`]: activeColumn === column,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th key={`query-results-table-header-${index}`}>
|
<p className="no-results-message">
|
||||||
|
Your live query returned no results.
|
||||||
<span>
|
<span>
|
||||||
<FleetIcon className={filterIconClassName} name="filter" />
|
Expecting to see results? Check to see if the hosts you targeted
|
||||||
{column}
|
reported “Online” or check out the “Errors”
|
||||||
|
table.
|
||||||
</span>
|
</span>
|
||||||
<InputField
|
</p>
|
||||||
name={column}
|
|
||||||
onChange={onFilterAttribute(filterable)}
|
|
||||||
onFocus={() => setActiveColumn(column)}
|
|
||||||
value={resultsFilter[filterable]}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTableHeaderRow = (rows: ICampaignQueryResult[]) => {
|
const renderTable = (tableData: ICampaignQueryResult[]) => {
|
||||||
if (!rows) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryAttrs = omit(rows[0], ["host_hostname"]);
|
|
||||||
const queryResultColumns = keys(queryAttrs);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<TableContainer
|
||||||
{renderTableHeaderColumn("hostname", -1)}
|
columns={resultsTableHeaders(tableData || [])}
|
||||||
{queryResultColumns.map((column, i) => {
|
data={tableData || []}
|
||||||
return renderTableHeaderColumn(column, i);
|
emptyComponent={renderNoResults}
|
||||||
})}
|
isLoading={false}
|
||||||
</tr>
|
disableCount
|
||||||
|
isClientSidePagination
|
||||||
|
showMarkAllPages={false}
|
||||||
|
isAllPagesSelected={false}
|
||||||
|
resultsTitle={"hosts"}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTableRows = (rows: ICampaignQueryResult[]) => {
|
const renderResultsTable = () => {
|
||||||
const filteredRows = filterArrayByHash(rows, resultsFilter);
|
|
||||||
|
|
||||||
return filteredRows.map((row: ICampaignQueryResult) => {
|
|
||||||
return (
|
|
||||||
<QueryResultsRow
|
|
||||||
key={row.uuid || row.host_hostname}
|
|
||||||
queryResult={row}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTable = () => {
|
|
||||||
const emptyResults = !queryResults || !queryResults.length;
|
const emptyResults = !queryResults || !queryResults.length;
|
||||||
const hasNoResultsYet = !isQueryFinished && emptyResults;
|
const hasNoResultsYet = !isQueryFinished && emptyResults;
|
||||||
const finishedWithNoResults =
|
const finishedWithNoResults =
|
||||||
@ -197,20 +159,11 @@ const QueryResults = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (finishedWithNoResults) {
|
if (finishedWithNoResults) {
|
||||||
return (
|
return renderNoResults();
|
||||||
<p className="no-results-message">
|
|
||||||
Your live query returned no results.
|
|
||||||
<span>
|
|
||||||
Expecting to see results? Check to see if the hosts you targeted
|
|
||||||
reported “Online” or check out the “Errors”
|
|
||||||
table.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${baseClass}__results-table-container`}>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className={`${baseClass}__export-btn`}
|
className={`${baseClass}__export-btn`}
|
||||||
onClick={onExportQueryResults}
|
onClick={onExportQueryResults}
|
||||||
@ -220,12 +173,7 @@ const QueryResults = ({
|
|||||||
Export results <img alt="" src={DownloadIcon} />
|
Export results <img alt="" src={DownloadIcon} />
|
||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
<div className={`${baseClass}__results-table-wrapper`}>
|
{renderTable(queryResults)}
|
||||||
<table className={`${baseClass}__table`}>
|
|
||||||
<thead>{renderTableHeaderRow(queryResults)}</thead>
|
|
||||||
<tbody>{renderTableRows(queryResults)}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -243,10 +191,7 @@ const QueryResults = ({
|
|||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
<div className={`${baseClass}__error-table-wrapper`}>
|
<div className={`${baseClass}__error-table-wrapper`}>
|
||||||
<table className={`${baseClass}__table`}>
|
{renderTable(errors)}
|
||||||
<thead>{renderTableHeaderRow(errors)}</thead>
|
|
||||||
<tbody>{renderTableRows(errors)}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -318,7 +263,7 @@ const QueryResults = ({
|
|||||||
{NAV_TITLES.ERRORS}
|
{NAV_TITLES.ERRORS}
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>{renderTable()}</TabPanel>
|
<TabPanel>{renderResultsTable()}</TabPanel>
|
||||||
<TabPanel>{renderErrorsTable()}</TabPanel>
|
<TabPanel>{renderErrorsTable()}</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</TabsWrapper>
|
</TabsWrapper>
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
/* 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 { ICampaignQueryResult } from "interfaces/campaign";
|
||||||
|
|
||||||
|
import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColumnFilter";
|
||||||
|
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||||
|
|
||||||
|
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 i = headers.findIndex((h) => h.id === "host_hostname");
|
||||||
|
if (i >= 0) {
|
||||||
|
// remove hostname header from headers
|
||||||
|
const [hostnameHeader] = newHeaders.splice(i, 1);
|
||||||
|
// reformat title and insert at start of headers array
|
||||||
|
newHeaders.unshift({ ...hostnameHeader, title: "hostname" });
|
||||||
|
}
|
||||||
|
return newHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultsTableHeaders = (results: ICampaignQueryResult[]): Column[] => {
|
||||||
|
// Table headers are derived from the shape of the first result.
|
||||||
|
// Note: It is possible that results may vary from the shape of the first result.
|
||||||
|
// For example, different versions of osquery may have new columns in a table
|
||||||
|
// However, this is believed to be a very unlikely scenario and there have been
|
||||||
|
// no reported issues.
|
||||||
|
const keys = results[0] ? Object.keys(results[0]) : [];
|
||||||
|
const headers = keys.map((key) => {
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
title: key,
|
||||||
|
Header: (headerProps: IHeaderProps) => (
|
||||||
|
<HeaderCell
|
||||||
|
value={headerProps.column.title || headerProps.column.id}
|
||||||
|
isSortedDesc={headerProps.column.isSortedDesc}
|
||||||
|
disableSortBy
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
accessor: key,
|
||||||
|
Cell: (cellProps: ICellProps) => cellProps?.cell?.value || null,
|
||||||
|
Filter: DefaultColumnFilter,
|
||||||
|
// filterType: "text",
|
||||||
|
disableSortBy: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return _unshiftHostname(headers);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resultsTableHeaders;
|
@ -1,6 +1,6 @@
|
|||||||
.query-results {
|
.query-results {
|
||||||
padding: $pad-xxxlarge $pad-xxlarge;
|
padding: $pad-xxxlarge $pad-xxlarge;
|
||||||
|
|
||||||
&__text-wrapper {
|
&__text-wrapper {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -64,6 +64,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__export-btn {
|
&__export-btn {
|
||||||
|
padding-top: 28px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 13px;
|
width: 13px;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
@ -73,81 +75,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__results-table-container,
|
.table-container {
|
||||||
&__error-table-container {
|
padding-top: $pad-large;
|
||||||
margin-top: $pad-large;
|
|
||||||
|
&__header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__results-table-wrapper,
|
.data-table__wrapper {
|
||||||
&__error-table-wrapper {
|
overflow-x: scroll;
|
||||||
width: calc(100vw - 80px); // minus padding left and right
|
|
||||||
border: solid 1px $ui-fleet-blue-15;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-top: $pad-large;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__table {
|
.data-table-container .data-table thead th {
|
||||||
background-color: $core-white;
|
min-width: 140px;
|
||||||
width: 100%;
|
padding-left: 0px;
|
||||||
box-sizing: border-box;
|
padding-right: $pad-large;
|
||||||
font-size: $x-small;
|
border-right: 0;
|
||||||
border-collapse: collapse;
|
|
||||||
color: $core-fleet-black;
|
&:first-of-type {
|
||||||
|
padding-left: $pad-large;
|
||||||
&__filter-icon {
|
|
||||||
color: $ui-fleet-black-25;
|
|
||||||
|
|
||||||
&--is-active {
|
|
||||||
color: $core-vibrant-blue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
thead {
|
|
||||||
background-color: $ui-off-white;
|
.data-table-container .data-table tbody td {
|
||||||
color: $core-fleet-black;
|
padding-left: 0px;
|
||||||
text-align: left;
|
padding-right: $pad-large;
|
||||||
border-bottom: 1px solid $ui-fleet-blue-15;
|
|
||||||
|
&:first-of-type {
|
||||||
th {
|
padding-left: $pad-large;
|
||||||
padding: 12px $pad-large;
|
|
||||||
min-width: 125px;
|
|
||||||
|
|
||||||
.form-field {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.fleeticon {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field {
|
|
||||||
width: 100%;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody {
|
|
||||||
background-color: $core-white;
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 12px $pad-large;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr {
|
|
||||||
border-bottom: 1px solid $ui-fleet-blue-15;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user