import React, { useCallback, useRef } from 'react';
import useDeepCompareEffect from 'use-deep-compare-effect';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import { isEqual } from 'lodash';
import { STATUS } from 'foremanReact/constants';
import { noop } from 'foremanReact/common/helpers';
import { useForemanSettings } from 'foremanReact/Root/Context/ForemanContext';
import { PaginationVariant, Flex, FlexItem } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';
import PageControls from './PageControls';
import MainTable from './MainTable';
import { getPageStats } from './helpers';
import Search from '../../components/Search';
import SelectAllCheckbox from '../SelectAllCheckbox';
import { orgId } from '../../services/api';
/* Patternfly 4 table wrapper */
const TableWrapper = ({
actionButtons,
toggleGroup,
children,
metadata,
fetchItems,
autocompleteEndpoint,
foremanApiAutoComplete,
searchQuery,
updateSearchQuery,
searchPlaceholderText,
additionalListeners,
activeFilters,
displaySelectAllCheckbox,
selectAll,
selectAllMode,
selectNone,
selectPage,
areAllRowsOnPageSelected,
areAllRowsSelected,
selectedCount,
selectedResults,
clearSelectedResults,
emptySearchBody,
disableSearch,
nodesBelowSearch,
bookmarkController,
...allTableProps
}) => {
const dispatch = useDispatch();
const foremanPerPage = useForemanSettings().perPage || 20;
const perPage = Number(metadata?.per_page ?? foremanPerPage);
const page = Number(metadata?.page ?? 1);
const total = Number(metadata?.subtotal ?? 0);
const { pageRowCount } = getPageStats({ total, page, perPage });
const unresolvedStatus = !!allTableProps?.status && allTableProps.status !== STATUS.RESOLVED;
const unresolvedStatusOrNoRows = unresolvedStatus || pageRowCount === 0;
const resolvedStatusNoContent =
!searchQuery && allTableProps.status === STATUS.RESOLVED && pageRowCount === 0;
const showPagination = !unresolvedStatusOrNoRows;
const showActionButtons = actionButtons && !unresolvedStatus;
const showToggleGroup = toggleGroup && !unresolvedStatus;
const paginationParams = useCallback(() =>
({ per_page: perPage, page }), [perPage, page]);
const prevRequest = useRef({});
const prevSearch = useRef('');
const prevAdditionalListeners = useRef([]);
const prevActiveFilters = useRef([]);
const paginationChangePending = useRef(null);
const hasChanged = (oldValue, newValue) => !isEqual(oldValue, newValue);
const spawnFetch = useCallback((paginationData) => {
const fetchWithParams = (allParams = {}) => {
const newRequest = {
...(paginationData ?? paginationParams()),
...allParams,
};
const pagParamsHaveChanged = (newPagParams, oldPagParams) =>
(newPagParams.page && hasChanged(newPagParams.page, oldPagParams.page)) ||
(newPagParams.per_page && hasChanged(newPagParams.per_page, oldPagParams.per_page));
const newRequestHasStalePagination = !!(paginationChangePending.current &&
pagParamsHaveChanged(newRequest, paginationChangePending.current));
const newRequestHasChanged = hasChanged(newRequest, prevRequest.current);
const additionalListenersHaveChanged =
hasChanged(additionalListeners, prevAdditionalListeners.current);
// If a pagination change is in-flight,
// don't send another request with stale data
if (newRequestHasStalePagination && !additionalListenersHaveChanged) return;
paginationChangePending.current = null;
if (newRequestHasChanged || additionalListenersHaveChanged) {
// don't fire the same request twice in a row
prevRequest.current = newRequest;
prevAdditionalListeners.current = additionalListeners;
dispatch(fetchItems(newRequest));
}
};
let paramsOverride;
const activeFiltersHaveChanged = hasChanged(activeFilters, prevActiveFilters.current);
const searchQueryHasChanged = hasChanged(searchQuery, prevSearch.current);
if (searchQuery && !disableSearch) paramsOverride = { search: searchQuery };
if (!disableSearch && (searchQueryHasChanged || activeFiltersHaveChanged)) {
// Reset page back to 1 when filter or search changes
prevSearch.current = searchQuery;
prevActiveFilters.current = activeFilters;
paramsOverride = { search: searchQuery, page: 1 };
}
if (paramsOverride) {
// paramsOverride may have both page and search, or just search
const pageOverride = !!paramsOverride.page;
if (pageOverride) paginationChangePending.current = null;
fetchWithParams(paramsOverride);
if (pageOverride) paginationChangePending.current = paramsOverride;
} else {
fetchWithParams();
}
}, [
disableSearch,
activeFilters,
dispatch,
fetchItems,
paginationParams,
searchQuery,
additionalListeners,
]);
useDeepCompareEffect(() => {
spawnFetch();
}, [searchQuery, spawnFetch, additionalListeners]);
const getAutoCompleteParams = search => ({
endpoint: autocompleteEndpoint,
params: {
organization_id: orgId(),
search,
},
});
// If the new page wouldn't exist because of a perPage change,
// we should set the current page to the last page.
const validatePagination = (data) => {
const mergedData = { ...paginationParams(), ...data };
const { page: requestedPage, per_page: newPerPage } = mergedData;
const { lastPage } = getPageStats({
page: requestedPage,
perPage: newPerPage,
total,
});
const result = {};
if (requestedPage) {
const newPage = (requestedPage > lastPage) ? lastPage : requestedPage;
result.page = Number(newPage);
}
if (newPerPage) result.per_page = Number(newPerPage);
return result;
};
const onPaginationUpdate = (updatedPagination) => {
const pagData = validatePagination(updatedPagination);
paginationChangePending.current = null;
spawnFetch(pagData);
paginationChangePending.current = pagData;
};
return (
<>
{displaySelectAllCheckbox &&
}
{!disableSearch && !resolvedStatusNoContent &&
updateSearchQuery(search)}
getAutoCompleteParams={getAutoCompleteParams}
foremanApiAutoComplete={foremanApiAutoComplete}
bookmarkController={bookmarkController}
placeholder={searchPlaceholderText}
/>
}
{showToggleGroup &&
{toggleGroup}
}
{showActionButtons &&
{actionButtons}
}
{showPagination &&
}
{nodesBelowSearch &&
{nodesBelowSearch}
}
{children}
{showPagination &&
}
>
);
};
TableWrapper.propTypes = {
searchQuery: PropTypes.string.isRequired,
updateSearchQuery: PropTypes.func.isRequired,
fetchItems: PropTypes.func.isRequired,
metadata: PropTypes.shape({
selectable: PropTypes.number,
total: PropTypes.number,
page: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string, // The API can sometimes return strings
]),
subtotal: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string, // The API can sometimes return strings
]),
per_page: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
search: PropTypes.string,
}),
autocompleteEndpoint: PropTypes.string.isRequired,
foremanApiAutoComplete: PropTypes.bool,
searchPlaceholderText: PropTypes.string,
actionButtons: PropTypes.node,
toggleGroup: PropTypes.node,
children: PropTypes.node,
// additionalListeners are anything that can trigger another API call, e.g. a filter
additionalListeners: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
PropTypes.bool,
])),
activeFilters: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
])),
defaultFilters: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
])),
displaySelectAllCheckbox: PropTypes.bool,
selectedCount: PropTypes.number,
selectedResults: PropTypes.arrayOf(PropTypes.shape({})),
clearSelectedResults: PropTypes.func,
selectAll: PropTypes.func,
selectAllMode: PropTypes.bool,
selectNone: PropTypes.func,
selectPage: PropTypes.func,
areAllRowsOnPageSelected: PropTypes.func,
areAllRowsSelected: PropTypes.func,
emptySearchBody: PropTypes.string,
disableSearch: PropTypes.bool,
nodesBelowSearch: PropTypes.node,
bookmarkController: PropTypes.string,
};
TableWrapper.defaultProps = {
metadata: { subtotal: 0, selectable: 0 },
children: null,
additionalListeners: [],
activeFilters: [],
defaultFilters: [],
foremanApiAutoComplete: false,
searchPlaceholderText: undefined,
actionButtons: null,
toggleGroup: null,
displaySelectAllCheckbox: false,
selectedCount: 0,
selectedResults: [],
clearSelectedResults: noop,
selectAll: undefined,
selectAllMode: false,
selectNone: undefined,
selectPage: undefined,
areAllRowsOnPageSelected: noop,
areAllRowsSelected: noop,
emptySearchBody: __('Try changing your search settings.'),
disableSearch: false,
nodesBelowSearch: null,
bookmarkController: undefined,
};
export default TableWrapper;