fleet/frontend/components/TableContainer/DataTable/DataTable.tsx
Martavis Parker 5d647025f9
1497 improved query experience (#1998)
* Step 1 for improving query experience (#1591)

* fake change to create draft PR

* temp routes to work and not modify old query page

* created new API abstraction for query

* refactored App.jsx to prepare react-query

* fixed flow of redirects after page refresh; functional component added

* setup for getting data on edit

* implementing functions for query page

* Old form showing on new setup

* improving and breaking up query form

* no need for the helpers anymore; clean up

* added type for button component variant

* step toward new save modal; have to switch gears to #1619

* creating new query works

* clean up

* linting cleanup

* added default value for new query

* will address dynamic save disabled in edit step

* Step 2 for improving query experience (select targets) (#1732)

* fake change to create draft PR

* temp routes to work and not modify old query page

* created new API abstraction for query

* refactored App.jsx to prepare react-query

* fixed flow of redirects after page refresh; functional component added

* setup for getting data on edit

* implementing functions for query page

* Old form showing on new setup

* improving and breaking up query form

* no need for the helpers anymore; clean up

* added type for button component variant

* step toward new save modal; have to switch gears to #1619

* creating new query works

* clean up

* linting cleanup

* added default value for new query

* split steps into separate files for readability

* components laid out

* new targets picker

* function clean up

* styling tables

* fixing logic

* fixed logic to keep getting related hosts

* formatting targets for API

* fixed default query

* clean up

* styled target selectors; fixed target input styles

* began total count

* forgot to remove debugging code

* lint fixes

* added target count from API

* clean up

* able to remove selected host targets from table

* lint fixes

* Improving query experience - Step 3 (query results) (#1766)

* fake change to create draft PR

* temp routes to work and not modify old query page

* created new API abstraction for query

* refactored App.jsx to prepare react-query

* fixed flow of redirects after page refresh; functional component added

* setup for getting data on edit

* implementing functions for query page

* Old form showing on new setup

* improving and breaking up query form

* no need for the helpers anymore; clean up

* added type for button component variant

* step toward new save modal; have to switch gears to #1619

* creating new query works

* clean up

* linting cleanup

* added default value for new query

* split steps into separate files for readability

* components laid out

* new targets picker

* function clean up

* styling tables

* fixing logic

* fixed logic to keep getting related hosts

* formatting targets for API

* fixed default query

* clean up

* styled target selectors; fixed target input styles

* began total count

* forgot to remove debugging code

* lint fixes

* added target count from API

* clean up

* able to remove selected host targets from table

* lint fixes

* connected run query with modern React/JS; clean up

* linting fixes

* fixed logic to retrieve results from live query

* linting fixes

* created new, simpler query progress

* populating results and errors tables as expected

* syntax fixes

* fixing styles for query results

* more styling for query results

* manual merge from main

* Rename core->free and basic->premium

* Fix lint js

* Comment out portion of test that seems to timeout

* Rename tier  to premium if basic is still loaded

* go sum

* Query Experience Cleanup Tasks (#1807)

* fixes to get merged main branch to build and work

* moved screens for query pages; clean up

* updated and typed react ace for query form; clean up

* using console error instead

* added real types instead of `any` except for errors

* query side panel ts and functional. prep for close task.

* ability to hide, show query table sidebar

* improved live query status warning

* added loading and error state for targets search

* error screen for targets; improved loading display

* now using API-created label for all linux

* missed some files on previous commit

* able to edit query

* clean up

* lint fixes

* query results showing as they come

* remove unused code

* removed old query page. major file cleanup.

* removed selectedTargets redux implementation

* removed unused redux actions and reducers

* removed unused keys in initial state

* selectedOsqueryTable is now using context API

* removed all querypages redux code

* set up context for app and user

* fixed auth with temp fix for wrapper

* completed redux removal from query page

* fixed var names coming from main branch

* fixed var name changes coming from issue 1501

* fixed save popup bug; clean up

* added permissions

* fixed login redirect

* removed unused props

* linting fix

* clean up

* removed unused component, refactor, and clean up

* fixed styles for step 1 as admin

* fixed styles for step 1 as observer

* fixed percentage of online hosts

* added loading progress to query stop button

* reset query status on run again

* added download icon to export button text

* fixed error reset on name input; fixed styles

* fixed bug where query value wasn't saving

* fixed query value when blank

* fixed bug - default query was running every time

* auto adding host from url to targets

* fixed flows for repeating run and save steps

* fleet ace is now TS and functional

* fixed a couple of tests

* fixed issues with query value text inconsistencies

* fixed query side panel not showing

* hiding error count if not > 0

* fixed showing editor for different roles

* using integer for targets

* go sum

* fixed targets param

* catching all errors while running query

* fixed hover state for title and description

* ignore unit test for now; lint fixes

* locking react-ace version

* ignoring tests breaking in github actions

* brought tests back

* fixing file name

* fixing file name again

* fixed e2e test

* have to ignore tests for now

* ignore certain premium tests for now

* one last test to revamp

* another test

* fixed teamflow test

* fixed observer query 403

* lint fixes

* fixed maintainer test

* added changes file

Co-authored-by: Tomas Touceda <chiiph@gmail.com>
2021-09-10 12:06:37 -07:00

363 lines
11 KiB
TypeScript

import React, { useMemo, useEffect, useCallback, useContext } from "react";
import { TableContext } from "context/table";
import PropTypes from "prop-types";
import classnames from "classnames";
import { useTable, useSortBy, useRowSelect, Row } from "react-table";
import { isString, kebabCase, noop } from "lodash";
import { useDeepEffect } from "utilities/hooks";
import Spinner from "components/loaders/Spinner";
import { ButtonVariant } from "components/buttons/Button/Button";
import Button from "../../buttons/Button";
import ActionButton, { IActionButtonProps } from "./ActionButton";
const baseClass = "data-table-container";
interface IDataTableProps {
columns: any;
data: any;
isLoading: boolean;
manualSortBy?: boolean;
sortHeader: any;
sortDirection: any;
onSort: any; // TODO: an event type
disableMultiRowSelect: boolean;
showMarkAllPages: boolean;
isAllPagesSelected: boolean; // TODO: make dependent on showMarkAllPages
toggleAllPagesSelected?: any; // TODO: an event type and make it dependent on showMarkAllPages
resultsTitle: string;
defaultPageSize: number;
primarySelectActionButtonVariant?: ButtonVariant;
primarySelectActionButtonIcon?: string;
primarySelectActionButtonText?: string | ((targetIds: number[]) => string);
onPrimarySelectActionClick: any; // figure out type
secondarySelectActions?: IActionButtonProps[];
onSelectSingleRow?: (value: Row) => void;
}
// This data table uses react-table for implementation. The relevant documentation of the library
// can be found here https://react-table.tanstack.com/docs/api/useTable
const DataTable = ({
columns: tableColumns,
data: tableData,
isLoading,
manualSortBy = false,
sortHeader,
sortDirection,
onSort,
disableMultiRowSelect,
showMarkAllPages,
isAllPagesSelected,
toggleAllPagesSelected,
resultsTitle,
defaultPageSize,
primarySelectActionButtonIcon,
primarySelectActionButtonVariant,
onPrimarySelectActionClick,
primarySelectActionButtonText,
secondarySelectActions,
onSelectSingleRow,
}: IDataTableProps): JSX.Element => {
const { resetSelectedRows } = useContext(TableContext);
const columns = useMemo(() => {
return tableColumns;
}, [tableColumns]);
// The table data needs to be ordered by the order we received from the API.
const data = useMemo(() => {
return tableData;
}, [tableData]);
const {
headerGroups,
rows,
prepareRow,
selectedFlatRows,
toggleAllRowsSelected,
isAllRowsSelected,
state: tableState,
} = useTable(
{
columns,
data,
initialState: {
sortBy: useMemo(() => {
return [{ id: sortHeader, desc: sortDirection === "desc" }];
}, [sortHeader, sortDirection]),
},
disableMultiSort: true,
disableSortRemove: true,
manualSortBy,
// Initializes as false, but changes briefly to true on successful notification
autoResetSelectedRows: resetSelectedRows,
sortTypes: React.useMemo(
() => ({
caseInsensitive: (a: any, b: any, id: any) => {
let valueA = a.values[id];
let valueB = b.values[id];
valueA = isString(valueA) ? valueA.toLowerCase() : valueA;
valueB = isString(valueB) ? valueB.toLowerCase() : valueB;
if (valueB > valueA) {
return 1;
}
if (valueB < valueA) {
return -1;
}
return 0;
},
}),
[]
),
},
useSortBy,
useRowSelect
);
const { sortBy, selectedRowIds } = tableState;
// This is used to listen for changes to sort. If there is a change
// Then the sortHandler change is fired.
useEffect(() => {
const column = sortBy[0];
if (column !== undefined) {
if (
column.id !== sortHeader ||
column.desc !== (sortDirection === "desc")
) {
onSort(column.id, column.desc);
}
} else {
onSort(undefined);
}
}, [sortBy, sortHeader, onSort, sortDirection]);
useEffect(() => {
if (isAllPagesSelected) {
toggleAllRowsSelected(true);
}
}, [isAllPagesSelected]);
useDeepEffect(() => {
if (
Object.keys(selectedRowIds).length < rows.length &&
toggleAllPagesSelected
) {
toggleAllPagesSelected(false);
}
}, [tableState.selectedRowIds, toggleAllPagesSelected]);
const onToggleAllPagesClick = useCallback(() => {
toggleAllPagesSelected();
}, [toggleAllPagesSelected]);
const onClearSelectionClick = useCallback(() => {
toggleAllRowsSelected(false);
toggleAllPagesSelected(false);
}, [toggleAllRowsSelected]);
const onSingleRowClick = useCallback(
(row) => {
if (disableMultiRowSelect) {
row.toggleRowSelected();
onSelectSingleRow && onSelectSingleRow(row);
toggleAllRowsSelected(false);
}
},
[disableMultiRowSelect]
);
const renderSelectedCount = (): JSX.Element => {
return (
<p>
<span>{selectedFlatRows.length}</span> selected
</p>
);
};
const renderAreAllSelected = (): JSX.Element | null => {
if (isAllPagesSelected) {
return <p>All matching {resultsTitle} are selected</p>;
}
if (isAllRowsSelected) {
return <p>All {resultsTitle} on this page are selected</p>;
}
return null;
};
const renderActionButton = (
actionButtonProps: IActionButtonProps
): JSX.Element => {
const {
name,
onActionButtonClick,
buttonText,
targetIds,
variant,
hideButton,
icon,
iconPosition,
} = actionButtonProps;
return (
<div className={`${baseClass}__${kebabCase(name)}`}>
<ActionButton
key={kebabCase(name)}
name={name}
buttonText={buttonText}
onActionButtonClick={onActionButtonClick || noop}
targetIds={targetIds}
variant={variant}
hideButton={hideButton}
icon={icon}
iconPosition={iconPosition}
/>
</div>
);
};
const renderPrimarySelectAction = (): JSX.Element | null => {
const targetIds = selectedFlatRows.map((row: any) => row.original.id);
const buttonText =
typeof primarySelectActionButtonText === "function"
? primarySelectActionButtonText(targetIds)
: primarySelectActionButtonText;
const name = buttonText ? kebabCase(buttonText) : "primary-select-action";
const actionProps = {
name,
buttonText: buttonText || "",
onActionButtonClick: onPrimarySelectActionClick,
targetIds,
variant: primarySelectActionButtonVariant,
icon: primarySelectActionButtonIcon,
};
return !buttonText ? null : renderActionButton(actionProps);
};
const renderSecondarySelectActions = (): JSX.Element[] | null => {
if (secondarySelectActions) {
const targetIds = selectedFlatRows.map((row: any) => row.original.id);
const buttons = secondarySelectActions.map((actionProps) => {
actionProps = { ...actionProps, targetIds };
return renderActionButton(actionProps);
});
return buttons;
}
return null;
};
const shouldRenderToggleAllPages =
Object.keys(selectedRowIds).length >= defaultPageSize &&
showMarkAllPages &&
!isAllPagesSelected;
return (
<div className={baseClass}>
<div className={"data-table data-table__wrapper"}>
{isLoading && (
<div className={"loading-overlay"}>
<Spinner />
</div>
)}
<table className={"data-table__table"}>
{Object.keys(selectedRowIds).length !== 0 && (
<thead className={"active-selection"}>
<tr {...headerGroups[0].getHeaderGroupProps()}>
<th
{...headerGroups[0].headers[0].getHeaderProps(
headerGroups[0].headers[0].getSortByToggleProps()
)}
>
{headerGroups[0].headers[0].render("Header")}
</th>
<th className={"active-selection__container"}>
<div className={"active-selection__inner"}>
{renderSelectedCount()}
<div className={"active-selection__inner-left"}>
{secondarySelectActions && renderSecondarySelectActions()}
</div>
<div className={"active-selection__inner-right"}>
{primarySelectActionButtonText &&
renderPrimarySelectAction()}
</div>
{toggleAllPagesSelected && renderAreAllSelected()}
{shouldRenderToggleAllPages && (
<Button
onClick={onToggleAllPagesClick}
variant={"text-link"}
className={"light-text"}
>
<>Select all matching {resultsTitle}</>
</Button>
)}
<Button
onClick={onClearSelectionClick}
variant={"text-link"}
>
Clear selection
</Button>
</div>
</th>
</tr>
</thead>
)}
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render("Header")}
</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map((row) => {
prepareRow(row);
const rowStyles = classnames({
"single-row": disableMultiRowSelect,
});
return (
<tr
className={rowStyles}
{...row.getRowProps({
// @ts-ignore // TS complains about prop not existing
onClick: () => {
disableMultiRowSelect && onSingleRowClick(row);
},
})}
>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
};
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;