Source

components/InventoryTable/EntityTableToolbar.js

import './EntityTableToolbar.scss';
import React, { Fragment, useEffect, useReducer } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import {
  Skeleton,
  SkeletonSize,
} from '@redhat-cloud-services/frontend-components/Skeleton';
import {
  mapGroups,
  tagsFilterReducer,
  tagsFilterState,
} from '@redhat-cloud-services/frontend-components/FilterHooks';
import { PrimaryToolbar } from '@redhat-cloud-services/frontend-components/PrimaryToolbar';
import {
  clearFilters,
  fetchAllTags,
  setFilter,
  toggleTagModal,
} from '../../store/actions';
import debounce from 'lodash/debounce';
import {
  HOST_GROUP_CHIP,
  LAST_SEEN_CHIP,
  OS_CHIP,
  REGISTERED_CHIP,
  RHCD_FILTER_KEY,
  STALE_CHIP,
  TAG_CHIP,
  TEXTUAL_CHIP,
  TEXT_FILTER,
  TagsModal,
  UPDATE_METHOD_KEY,
  SYSTEM_TYPE_KEY,
  arrayToSelection,
  reduceFilters,
} from '../../Utilities/index';
import { onDeleteFilter, onDeleteGroupFilter, onDeleteTag } from './helpers';
import {
  filtersReducer,
  groupFilterReducer,
  groupFilterState,
  lastSeenFilterReducer,
  lastSeenFilterState,
  operatingSystemFilterReducer,
  operatingSystemFilterState,
  registeredWithFilterReducer,
  registeredWithFilterState,
  rhcdFilterReducer,
  rhcdFilterState,
  stalenessFilterReducer,
  stalenessFilterState,
  textFilterReducer,
  textFilterState,
  updateMethodFilterReducer,
  updateMethodFilterState,
  useLastSeenFilter,
  useOperatingSystemFilter,
  useRegisteredWithFilter,
  useRhcdFilter,
  useStalenessFilter,
  useTagsFilter,
  useTextFilter,
  useUpdateMethodFilter,
  useSystemTypeFilter,
  systemTypeFilterReducer,
  systemTypeFilterState,
} from '../filters';
import useFeatureFlag from '../../Utilities/useFeatureFlag';
import useGroupFilter from '../filters/useGroupFilter';
import { DatePicker, Split, SplitItem } from '@patternfly/react-core';
import { fromValidator, UNIX_EPOCH, toValidator } from '../filters/helpers';
import useInventoryExport from './hooks/useInventoryExport/useInventoryExport';

/**
 * Table toolbar used at top of inventory table.
 * It uses couple of filters and acces redux data along side all passed props.
 *  @param {*} props used in this component.
 */
const EntityTableToolbar = ({
  total,
  page,
  perPage,
  filterConfig,
  hasItems,
  children,
  actionsConfig,
  activeFiltersConfig,
  showTags,
  getTags,
  items,
  sortBy,
  customFilters,
  hasAccess,
  bulkSelect,
  hideFilters,
  paginationProps,
  onRefreshData,
  loaded,
  showTagModal,
  showSystemTypeFilter,
  showCentosVersions,
  showNoGroupOption,
  enableExport,
  fetchCustomOSes,
  ...props
}) => {
  const dispatch = useDispatch();
  const reducer = useReducer(
    filtersReducer([
      textFilterReducer,
      stalenessFilterReducer,
      registeredWithFilterReducer,
      tagsFilterReducer,
      operatingSystemFilterReducer,
      rhcdFilterReducer,
      lastSeenFilterReducer,
      updateMethodFilterReducer,
      groupFilterReducer,
      systemTypeFilterReducer,
    ]),
    {
      ...textFilterState,
      ...stalenessFilterState,
      ...registeredWithFilterState,
      ...tagsFilterState,
      ...operatingSystemFilterState,
      ...rhcdFilterState,
      ...updateMethodFilterState,
      ...lastSeenFilterState,
      ...groupFilterState,
      ...systemTypeFilterState,
    },
  );
  const activeFilters = useSelector(
    ({ entities: { activeFilters } }) => activeFilters,
  );
  const allTagsLoaded = useSelector(
    ({ entities: { allTagsLoaded } }) => allTagsLoaded,
  );
  const allTags = useSelector(({ entities: { allTags } }) => allTags);
  const additionalTagsCount = useSelector(
    ({ entities: { additionalTagsCount } }) => additionalTagsCount,
  );
  const [nameFilter, nameChip, textFilter, setTextFilter] =
    useTextFilter(reducer);
  const [stalenessFilter, stalenessChip, staleFilter, setStaleFilter] =
    useStalenessFilter(reducer);
  const [
    registeredFilter,
    registeredChip,
    registeredWithFilter,
    setRegisteredWithFilter,
  ] = useRegisteredWithFilter(reducer);
  const [
    rhcdFilterConfig,
    rhcdFilterChips,
    rhcdFilterValue,
    setRhcdFilterValue,
  ] = useRhcdFilter(reducer);
  const [
    lastSeenFilter,
    lastSeenChip,
    lastSeenFilterValue,
    setLastSeenFilterValue,
    onFromChange,
    onToChange,
    endDate,
    startDate,
    setStartDate,
    setEndDate,
  ] = useLastSeenFilter(reducer);
  const [osFilterConfig, osFilterChips, osFilterValue, setOsFilterValue] =
    useOperatingSystemFilter(
      reducer,
      [],
      hasAccess,
      showCentosVersions,
      fetchCustomOSes,
    );
  const [
    updateMethodConfig,
    updateMethodChips,
    updateMethodValue,
    setUpdateMethodValue,
  ] = useUpdateMethodFilter(reducer);

  const isKesselEnabled = useFeatureFlag('hbi.kessel-migration');
  const [hostGroupConfig, hostGroupChips, hostGroupValue, setHostGroupValue] =
    useGroupFilter(showNoGroupOption, isKesselEnabled);

  const isUpdateMethodEnabled = useFeatureFlag('hbi.ui.system-update-method');
  const { tagsFilter, tagsChip, selectedTags, setSelectedTags, filterTagsBy } =
    useTagsFilter(
      allTags,
      allTagsLoaded,
      additionalTagsCount,
      () => dispatch(toggleTagModal(true)),
      reducer,
    );

  const [
    systemTypeConfig,
    systemTypeChips,
    systemTypeValue,
    setSystemTypeValue,
  ] = useSystemTypeFilter(reducer);
  /**
   * Debounced function for fetching all tags.
   */
  const debounceGetAllTags = debounce((config, options) => {
    if (showTags && !hasItems && hasAccess) {
      dispatch(
        fetchAllTags(
          config,
          {
            ...options?.paginationhideFilters,
          },
          getTags,
        ),
      );
    }
  }, 800);

  const enabledFilters = {
    name: !(hideFilters.all && hideFilters.name !== false) && !hideFilters.name,
    stale:
      !(hideFilters.all && hideFilters.stale !== false) && !hideFilters.stale,
    registeredWith:
      !(hideFilters.all && hideFilters.registeredWith !== false) &&
      !hideFilters.registeredWith,
    operatingSystem:
      !(hideFilters.all && hideFilters.operatingSystem !== false) &&
      !hideFilters.operatingSystem,
    tags: !(hideFilters.all && hideFilters.tags !== false) && !hideFilters.tags,
    rhcdFilter:
      !(hideFilters.all && hideFilters.rhcdFilter !== false) &&
      !hideFilters.rhcdFilter,
    lastSeenFilter:
      !(hideFilters.all && hideFilters.lastSeen !== false) &&
      !hideFilters.lastSeen,
    //hides the filter untill API is ready. JIRA: RHIF-169
    updateMethodFilter:
      isUpdateMethodEnabled &&
      !(hideFilters.all && hideFilters.updateMethodFilter !== false) &&
      !hideFilters.updateMethodFilter,
    hostGroupFilter:
      !(hideFilters.all && hideFilters.hostGroupFilter !== false) &&
      !hideFilters.hostGroupFilter,
    systemTypeFilter:
      !(hideFilters.all && hideFilters.systemTypeFilter !== false) &&
      !hideFilters.systemTypeFilter,
  };

  const exportConfig = useInventoryExport({
    filters: {
      ...activeFilters,
      ...customFilters,
    },
  });

  /**
   * Function to dispatch load systems and fetch all tags.
   *  @param options
   */
  const onRefreshDataInner = (options) => {
    if (hasAccess) {
      onRefreshData(options);
      if (showTags && !hasItems) {
        dispatch(fetchAllTags(filterTagsBy, {}, getTags));
      }
    }
  };

  /**
   * Function used to update data, it either calls `onRefresh` from props or dispatches `onRefreshData`.
   * `onRefresh` function takes two parameters
   * entire config with new changes.
   * callback to update data.
   *  @param {*} config new config to fetch data.
   */
  const updateData = (config) => {
    if (hasAccess) {
      onRefreshDataInner(config);
    }
  };

  /**
   * Debounced `updateData` function.
   */
  const debouncedRefresh = debounce((config) => updateData(config), 800);

  /**
   * Component did mount effect to calculate actual filters from redux.
   */
  useEffect(() => {
    const {
      textFilter,
      tagFilters,
      staleFilter,
      registeredWithFilter,
      osFilter,
      rhcdFilter,
      lastSeenFilter,
      updateMethodFilter,
      hostGroupFilter,
      systemTypeFilter,
    } = reduceFilters([
      ...(activeFilters || []),
      ...(customFilters?.filters || []),
    ]);

    debouncedRefresh();
    enabledFilters.name && setTextFilter(textFilter);
    enabledFilters.stale && setStaleFilter(staleFilter);
    enabledFilters.registeredWith &&
      setRegisteredWithFilter(registeredWithFilter);
    enabledFilters.tags && setSelectedTags(tagFilters);
    enabledFilters.operatingSystem && setOsFilterValue(osFilter);
    enabledFilters.rhcdFilter && setRhcdFilterValue(rhcdFilter);
    enabledFilters.updateMethodFilter &&
      setUpdateMethodValue(updateMethodFilter);
    enabledFilters.lastSeenFilter && setLastSeenFilterValue(lastSeenFilter);
    enabledFilters.hostGroupFilter && setHostGroupValue(hostGroupFilter);
    enabledFilters.systemTypeFilter && setSystemTypeValue(systemTypeFilter);
  }, []);

  /**
   * Function used to change text filter.
   *  @param {*} value     new value used for filtering.
   *  @param {*} debounced if debounce function should be used.
   */
  const onSetTextFilter = (value, debounced = true) => {
    const trimmedValue = value?.trim();

    const textualFilter = activeFilters?.find(
      (oneFilter) => oneFilter.value === TEXT_FILTER,
    );
    if (textualFilter) {
      textualFilter.filter = trimmedValue;
    } else {
      if (trimmedValue) {
        // TODO This is sus
        activeFilters?.push({ value: TEXT_FILTER, filter: trimmedValue });
      }
    }

    const refresh = debounced ? debouncedRefresh : updateData;
    refresh({ page: 1, perPage, filters: activeFilters });
  };

  /**
   * General function to apply filter (excluding tag and text).
   *  @param {*} value     new value to be set of specified filter.
   *  @param {*} filterKey which filter should be changed.
   *  @param {*} refresh   refresh callback function.
   */
  const onSetFilter = (value, filterKey, refresh) => {
    const newFilters = [
      ...(activeFilters || []).filter(
        (oneFilter) =>
          !Object.prototype.hasOwnProperty.call(oneFilter, filterKey),
      ),
      { [filterKey]: value },
    ];
    refresh({ page: 1, perPage, filters: newFilters });
  };

  const shouldReload = page && perPage && activeFilters && (!hasItems || items);

  useEffect(() => {
    if (shouldReload && showTags && enabledFilters.tags) {
      debounceGetAllTags(filterTagsBy);
    }
  }, [filterTagsBy, customFilters?.tags]);

  useEffect(() => {
    if (shouldReload && enabledFilters.name) {
      onSetTextFilter(textFilter, true);
    }
  }, [textFilter]);

  useEffect(() => {
    if (shouldReload && enabledFilters.stale) {
      onSetFilter(staleFilter, 'staleFilter', debouncedRefresh);
    }
  }, [staleFilter]);

  useEffect(() => {
    if (shouldReload && enabledFilters.registeredWith) {
      onSetFilter(
        registeredWithFilter,
        'registeredWithFilter',
        debouncedRefresh,
      );
    }
  }, [registeredWithFilter]);

  useEffect(() => {
    if (shouldReload && showTags && enabledFilters.tags) {
      onSetFilter(mapGroups(selectedTags), 'tagFilters', debouncedRefresh);
    }
  }, [selectedTags]);

  useEffect(() => {
    if (shouldReload && enabledFilters.operatingSystem) {
      onSetFilter(osFilterValue, 'osFilter', debouncedRefresh);
    }
  }, [osFilterValue]);

  useEffect(() => {
    if (shouldReload && enabledFilters.rhcdFilter) {
      onSetFilter(rhcdFilterValue, 'rhcdFilter', debouncedRefresh);
    }
  }, [rhcdFilterValue]);

  useEffect(() => {
    if (shouldReload && enabledFilters.lastSeenFilter) {
      onSetFilter(lastSeenFilterValue, 'lastSeenFilter', debouncedRefresh);
    }
  }, [lastSeenFilterValue]);

  useEffect(() => {
    if (shouldReload && enabledFilters.updateMethodFilter) {
      onSetFilter(updateMethodValue, 'updateMethodFilter', debouncedRefresh);
    }
  }, [updateMethodValue]);

  useEffect(() => {
    if (shouldReload && enabledFilters.hostGroupFilter) {
      onSetFilter(hostGroupValue, 'hostGroupFilter', debouncedRefresh);
    }
  }, [hostGroupValue]);

  useEffect(() => {
    if (shouldReload && enabledFilters.systemTypeFilter) {
      onSetFilter(systemTypeValue, 'systemTypeFilter', debouncedRefresh);
    }
  }, [systemTypeValue]);

  /**
   * Mapper to simplify removing of any filter.
   */
  const deleteMapper = {
    [TEXTUAL_CHIP]: () => setTextFilter(''),
    [TAG_CHIP]: (deleted) =>
      setSelectedTags(
        onDeleteTag(deleted, selectedTags, (selectedTags) =>
          onSetFilter(mapGroups(selectedTags), 'tagFilters', updateData),
        ),
      ),
    [STALE_CHIP]: (deleted) =>
      setStaleFilter(onDeleteFilter(deleted, staleFilter)),
    [REGISTERED_CHIP]: (deleted) =>
      setRegisteredWithFilter(onDeleteFilter(deleted, registeredWithFilter)),
    [OS_CHIP]: (deleted) =>
      setOsFilterValue(onDeleteGroupFilter(deleted, osFilterValue)),
    [RHCD_FILTER_KEY]: (deleted) =>
      setRhcdFilterValue(onDeleteFilter(deleted, rhcdFilterValue)),
    [LAST_SEEN_CHIP]: (deleted) => {
      setLastSeenFilterValue(
        onDeleteFilter(deleted, [lastSeenFilterValue.mark]),
      ),
        setStartDate(),
        setEndDate();
    },
    [UPDATE_METHOD_KEY]: (deleted) =>
      setUpdateMethodValue(onDeleteFilter(deleted, updateMethodValue)),
    [HOST_GROUP_CHIP]: (deleted) =>
      setHostGroupValue(onDeleteFilter(deleted, hostGroupValue)),
    [SYSTEM_TYPE_KEY]: (deleted) =>
      setSystemTypeValue(onDeleteFilter(deleted, systemTypeValue)),
  };
  /**
   * Function to reset all filters with 'Reset Filter' is clicked
   */
  const resetFilters = () => {
    enabledFilters.name && setTextFilter('');
    enabledFilters.stale && setStaleFilter([]);
    enabledFilters.registeredWith && setRegisteredWithFilter([]);
    enabledFilters.tags && setSelectedTags({});
    enabledFilters.operatingSystem && setOsFilterValue([]);
    enabledFilters.rhcdFilter && setRhcdFilterValue([]);
    enabledFilters.lastSeenFilter && setLastSeenFilterValue([]);
    enabledFilters.updateMethodFilter && setUpdateMethodValue([]);
    enabledFilters.hostGroupFilter && setHostGroupValue([]);
    enabledFilters.systemTypeFilter && setSystemTypeValue([]);
    setEndDate();
    setStartDate(UNIX_EPOCH);
    dispatch(setFilter([]));
    updateData({ page: 1, filters: [] });
  };

  /**
   * Function to create active filters chips.
   */
  const constructFilters = () => {
    return {
      ...(activeFiltersConfig || {}),
      filters: [
        ...(showTags && !hasItems && enabledFilters.tags ? tagsChip : []),
        ...(!hasItems && enabledFilters.name ? nameChip : []),
        ...(!hasItems && enabledFilters.stale ? stalenessChip : []),
        ...(!hasItems && enabledFilters.registeredWith ? registeredChip : []),
        ...(!hasItems && enabledFilters.operatingSystem ? osFilterChips : []),
        ...(!hasItems && enabledFilters.rhcdFilter ? rhcdFilterChips : []),
        ...(!hasItems && enabledFilters.updateMethodFilter
          ? updateMethodChips
          : []),
        ...(!hasItems && enabledFilters.lastSeenFilter ? lastSeenChip : []),
        ...(!hasItems && enabledFilters.hostGroupFilter ? hostGroupChips : []),
        ...(showSystemTypeFilter && !hasItems && enabledFilters.systemTypeFilter
          ? systemTypeChips
          : []),
        ...(activeFiltersConfig?.filters || []),
      ],
      onDelete: (e, [deleted, ...restDeleted], isAll) => {
        if (isAll) {
          dispatch(clearFilters());
          resetFilters();
        } else {
          deleteMapper[deleted.type]?.(deleted);
        }

        activeFiltersConfig &&
          activeFiltersConfig.onDelete &&
          activeFiltersConfig.onDelete(e, [deleted, ...restDeleted], isAll);
      },
    };
  };

  const inventoryFilters = [
    ...(!hasItems
      ? [
          ...(enabledFilters.name ? [nameFilter] : []),
          ...(enabledFilters.stale ? [stalenessFilter] : []),
          ...(enabledFilters.operatingSystem ? [osFilterConfig] : []),
          ...(enabledFilters.registeredWith ? [registeredFilter] : []),
          ...(enabledFilters.rhcdFilter ? [rhcdFilterConfig] : []),
          ...(enabledFilters.updateMethodFilter ? [updateMethodConfig] : []),
          ...(enabledFilters.lastSeenFilter ? [lastSeenFilter] : []),
          ...(enabledFilters.hostGroupFilter ? [hostGroupConfig] : []),
          ...(showSystemTypeFilter && enabledFilters.systemTypeFilter
            ? [systemTypeConfig]
            : []),
          ...(showTags && enabledFilters.tags ? [tagsFilter] : []),
        ]
      : []),
    ...(filterConfig?.items || []),
  ];
  const preselectedTags = Object.entries(selectedTags).reduce(
    (sTags, [namespace, tag]) => {
      const tags = Object.values(tag).map(
        ({
          item: {
            meta: {
              tag: { key, value },
            },
          },
        }) => ({
          id: `${namespace}/${key}=${value}`,
          cells: [key, value, namespace],
          item: {
            meta: {
              tag: { key, value },
            },
          },
        }),
      );

      return [...sTags, ...tags];
    },
    [],
  );

  return (
    <Fragment>
      <PrimaryToolbar
        {...props}
        {...(bulkSelect && {
          bulkSelect: {
            ...bulkSelect,
            isDisabled: bulkSelect?.isDisabled || !hasAccess,
          },
        })}
        className={`ins-c-inventory__table--toolbar ${
          hasItems || !inventoryFilters.length
            ? 'ins-c-inventory__table--toolbar-has-items'
            : ''
        }`}
        {...(inventoryFilters?.length > 0 && {
          filterConfig: {
            ...(filterConfig || {}),
            isDisabled: !hasAccess,
            items: inventoryFilters?.map((filter) => ({
              ...filter,
              filterValues: {
                placeholder:
                  filter?.filterValues?.placeholder ||
                  `Filter by ${filter?.label?.toLowerCase()}`,
                ...filter?.filterValues,
                isDisabled: !hasAccess,
              },
            })),
          },
        })}
        {...(hasAccess && { activeFiltersConfig: constructFilters() })}
        actionsConfig={loaded ? actionsConfig : null}
        pagination={
          loaded ? (
            {
              page,
              itemCount: !hasAccess ? 0 : total,
              isDisabled: !hasAccess,
              perPage,
              onSetPage: (_e, newPage) => onRefreshData({ page: newPage }),
              onPerPageSelect: (_e, newPerPage) =>
                onRefreshData({ page: 1, per_page: newPerPage }),
              titles: {
                optionsToggleAriaLabel: 'Items per page',
              },
              ...paginationProps,
            }
          ) : (
            <Skeleton size={SkeletonSize.lg} />
          )
        }
        exportConfig={
          props.exportConfig ? props.exportConfig : enableExport && exportConfig
        }
      >
        {lastSeenFilterValue?.mark === 'custom' && (
          <Split>
            <SplitItem>
              <DatePicker
                onChange={onFromChange}
                aria-label="Start date"
                validators={[fromValidator(endDate)]}
                placeholder="Start"
              />
            </SplitItem>
            <SplitItem style={{ padding: '6px 12px 0 12px' }}>to</SplitItem>
            <SplitItem>
              <DatePicker
                value={endDate}
                onChange={onToChange}
                rangeStart={
                  startDate === UNIX_EPOCH ? new Date() : new Date(startDate)
                }
                validators={[toValidator(startDate)]}
                aria-label="End date"
                placeholder="End"
              />
            </SplitItem>
          </Split>
        )}
        {children}
      </PrimaryToolbar>
      {(showTags || enabledFilters.tags || showTagModal) && (
        <TagsModal
          selected={preselectedTags}
          filterTagsBy={filterTagsBy}
          onApply={(selected) => setSelectedTags(arrayToSelection(selected))}
          getTags={getTags}
        />
      )}
    </Fragment>
  );
};

EntityTableToolbar.propTypes = {
  showTags: PropTypes.bool,
  getTags: PropTypes.func,
  hasAccess: PropTypes.bool,
  filterConfig: PropTypes.object,
  total: PropTypes.number,
  hasItems: PropTypes.bool,
  page: PropTypes.number,
  onClearFilters: PropTypes.func,
  toggleTagModal: PropTypes.func,
  perPage: PropTypes.number,
  children: PropTypes.node,
  pagination: PropTypes.shape({
    page: PropTypes.number,
    perPage: PropTypes.number,
  }),
  actionsConfig: PropTypes.object,
  activeFiltersConfig: PropTypes.object,
  onRefreshData: PropTypes.func,
  customFilters: PropTypes.shape({
    tags: PropTypes.oneOfType([
      PropTypes.object,
      PropTypes.arrayOf(PropTypes.string),
    ]),
    filters: PropTypes.array,
  }),
  hideFilters: PropTypes.shape({
    tags: PropTypes.bool,
    name: PropTypes.bool,
    registeredWith: PropTypes.bool,
    stale: PropTypes.bool,
    operatingSystem: PropTypes.bool,
    rhcdFilter: PropTypes.bool,
    lastSeen: PropTypes.bool,
    updateMethodFilter: PropTypes.bool,
    hostGroupFilter: PropTypes.bool,
    systemTypeFilter: PropTypes.bool,
    all: PropTypes.bool,
  }),
  paginationProps: PropTypes.object,
  loaded: PropTypes.bool,
  onRefresh: PropTypes.func,
  hasCheckbox: PropTypes.bool,
  isLoaded: PropTypes.bool,
  items: PropTypes.array,
  sortBy: PropTypes.object,
  bulkSelect: PropTypes.object,
  showTagModal: PropTypes.bool,
  disableDefaultColumns: PropTypes.any,
  showCentosVersions: PropTypes.bool,
  showSystemTypeFilter: PropTypes.bool,
  showNoGroupOption: PropTypes.bool,
  enableExport: PropTypes.bool,
  exportConfig: PropTypes.object,
  fetchCustomOSes: PropTypes.func,
};

EntityTableToolbar.defaultProps = {
  showTags: false,
  hasAccess: true,
  activeFiltersConfig: {},
  hideFilters: {},
  showNoGroupOption: false,
};

export default EntityTableToolbar;