fleet/frontend/components/TableContainer/TableContainer.tsx
Gabe Hernandez 1f97aebf61
implement user table with new table and hook up create and edit and delete users (#587)
* hook up user and invite data together for data table

* added client derived data to user table

* hooked up action dropdown to table

* hooked up edit modal and password reset

* started adding editing user functiaonlity

* add query params to /invite call

* clean up editing teams

* update select team from to handle existing users with teams

* update closes modal now

* reuse getUser to clean up code in userManagementpage

* pass form data to updating user that is not the current User

* add dynamic userform submit text and fix tests

* fix lint error in table component

* added empty state for user table

* clean up unused data table props

* added delete modal

* add delete user functionality

* add delete option for invite

* Add styles for rows in user table and action dropdown cell

* hook up user and invite data together for data table

* added client derived data to user table

* hooked up action dropdown to table

* hooked up edit modal and password reset

* started adding editing user functiaonlity

* add query params to /invite call

* clean up editing teams

* update select team from to handle existing users with teams

* update closes modal now

* reuse getUser to clean up code in userManagementpage

* pass form data to updating user that is not the current User

* add dynamic userform submit text and fix tests

* fix lint error in table component

* added empty state for user table

* clean up unused data table props

* added delete modal

* add delete user functionality

* add delete option for invite

* Merge in generateClassTag

* Refactor table styles

* Add newline to DropdownCell style sheet

Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>

* update checking for admin through global_role in nav

* added defaultGloablRole for creat user modal

* remove unused comment

* fix broken tests

* reenabled search for users tests

Co-authored-by: noahtalerman <47070608+noahtalerman@users.noreply.github.com>
2021-04-09 11:44:57 +01:00

203 lines
6.1 KiB
TypeScript

import React, { useState, useEffect, useCallback, useRef } from 'react';
import classnames from 'classnames';
import { useAsyncDebounce } from 'react-table';
import Button from 'components/buttons/Button';
// ignore TS error for now until these are rewritten in ts.
// @ts-ignore
import InputField from 'components/forms/fields/InputField';
// @ts-ignore
import KolideIcon from 'components/icons/KolideIcon';
// @ts-ignore
import Pagination from 'components/Pagination';
// @ts-ignore
import scrollToTop from 'utilities/scroll_to_top';
// @ts-ignore
import DataTable from './DataTable/DataTable';
import TableContainerUtils from './TableContainerUtils';
interface ITableQueryData {
searchQuery: string;
sortHeader: string;
sortDirection: string;
pageSize: number;
pageIndex: number;
}
interface ITableContainerProps<T, U> {
columns: T[];
data: U[];
isLoading: boolean;
defaultSortHeader: string;
defaultSortDirection: string;
onActionButtonClick: () => void;
actionButtonText: string;
onQueryChange: (queryData: ITableQueryData) => void;
inputPlaceHolder: string;
includesTableActionButton?: boolean; // will be used later to conditionally show button
disableActionButton?: boolean;
resultsTitle?: string;
additionalQueries?: string;
emptyComponent: React.ElementType;
className?: string;
}
const baseClass = 'table-container';
const DEFAULT_PAGE_SIZE = 100;
const DEFAULT_PAGE_INDEX = 0;
const DEBOUNCE_QUERY_DELAY = 300;
const TableContainer = <T, U>(props: ITableContainerProps<T, U>): JSX.Element => {
const {
columns,
data,
isLoading,
defaultSortHeader,
defaultSortDirection,
onActionButtonClick,
inputPlaceHolder,
additionalQueries,
onQueryChange,
resultsTitle,
emptyComponent,
className,
disableActionButton,
actionButtonText,
} = props;
const [searchQuery, setSearchQuery] = useState('');
const [sortHeader, setSortHeader] = useState(defaultSortHeader || '');
const [sortDirection, setSortDirection] = useState(defaultSortDirection || '');
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
const [pageIndex, setPageIndex] = useState(DEFAULT_PAGE_INDEX);
const wrapperClasses = classnames(baseClass, className);
const EmptyComponent = emptyComponent;
const onSortChange = useCallback((id?:string, isDesc?: boolean) => {
if (id === undefined) {
setSortHeader('');
setSortDirection('');
} else {
setSortHeader(id);
const direction = isDesc ? 'desc' : 'asc';
setSortDirection(direction);
}
}, [setSortHeader, setSortDirection]);
const onSearchQueryChange = (value: string) => {
setSearchQuery(value);
};
const hasPageIndexChangedRef = useRef(false);
const onPaginationChange = (newPage: number) => {
setPageIndex(newPage);
hasPageIndexChangedRef.current = true;
scrollToTop();
};
// We use useRef to keep track of the previous searchQuery value. This allows us
// to later compare this the the current value and debounce a change handler.
const prevSearchQueryRef = useRef(searchQuery);
const prevSearchQuery = prevSearchQueryRef.current;
const debounceOnQueryChange = useAsyncDebounce((queryData: ITableQueryData) => {
onQueryChange(queryData);
}, DEBOUNCE_QUERY_DELAY);
// When any of our query params change, or if any additionalQueries change, we want to fire off
// the parent components handler function with this updated query data. There is logic in here to check
// different types of query updates, as we handle some of them differently than others.
useEffect(() => {
const queryData = {
searchQuery,
sortHeader,
sortDirection,
pageSize,
pageIndex,
};
// Something besides the pageIndex has changed; we want to set it back to 0.
if (!hasPageIndexChangedRef.current) {
const updateQueryData = {
...queryData,
pageIndex: 0,
};
// searchQuery has changed; we want to debounce calling the handler so the
// user can finish typing.
if (searchQuery !== prevSearchQuery) {
debounceOnQueryChange(updateQueryData);
} else {
onQueryChange(updateQueryData);
}
setPageIndex(0);
} else {
onQueryChange(queryData);
}
hasPageIndexChangedRef.current = false;
}, [searchQuery, sortHeader, sortDirection, pageSize, pageIndex, additionalQueries, onQueryChange]);
return (
<div className={wrapperClasses}>
<div className={`${baseClass}__header`}>
{ data && data.length ?
<p className={`${baseClass}__results-count`}>
{TableContainerUtils.generateResultsCountText(resultsTitle, pageIndex, pageSize, data.length)}
</p> :
<p />
}
<div className={`${baseClass}__table-controls`}>
<Button
disabled={disableActionButton}
onClick={onActionButtonClick}
variant="unstyled"
className={`${baseClass}__table-action-button`}
>
{actionButtonText}
</Button>
<div className={`${baseClass}__search-input`}>
<InputField
placeholder={inputPlaceHolder}
name="searchQuery"
onChange={onSearchQueryChange}
value={searchQuery}
inputWrapperClass={`${baseClass}__input-wrapper`}
/>
<KolideIcon name="search" />
</div>
</div>
</div>
<div className={`${baseClass}__data-table-container`}>
{/* No entities for this result. */}
{!isLoading && data.length === 0 ?
<EmptyComponent /> :
<>
<DataTable
isLoading={isLoading}
columns={columns}
data={data}
sortHeader={sortHeader}
sortDirection={sortDirection}
onSort={onSortChange}
/>
<Pagination
resultsOnCurrentPage={data.length}
currentPage={pageIndex}
resultsPerPage={pageSize}
onPaginationChange={onPaginationChange}
/>
</>
}
</div>
</div>
);
};
export default TableContainer;