Katello/katello

View on GitHub
webpack/scenes/ContentViews/Details/Filters/CVPackageGroupFilterContent.js

Summary

Maintainability
F
4 days
Test Coverage
import React, { useState, useEffect, useCallback } from 'react';
import useDeepCompareEffect from 'use-deep-compare-effect';
import PropTypes from 'prop-types';
import { shallowEqual, useSelector, useDispatch } from 'react-redux';
import { TableVariant } from '@patternfly/react-table';
import {
  Tabs, Tab, TabTitleText, Split, SplitItem, Button,
  Select, SelectVariant, SelectOption, Dropdown, DropdownItem, KebabToggle,
} from '@patternfly/react-core';
import { STATUS } from 'foremanReact/constants';
import { translate as __ } from 'foremanReact/common/I18n';

import onSelect from '../../../../components/Table/helpers';
import TableWrapper from '../../../../components/Table/TableWrapper';
import {
  selectCVFilterPackageGroups,
  selectCVFilterPackageGroupStatus,
  selectCVFilterPackageGroupError,
  selectCVFilters, selectCVFilterDetails,
} from '../ContentViewDetailSelectors';
import AddedStatusLabel from '../../../../components/AddedStatusLabel';
import getContentViewDetails, {
  addCVFilterRule, removeCVFilterRule, getCVFilterPackageGroups,
  deleteContentViewFilterRules, addContentViewFilterRules,
} from '../ContentViewDetailActions';
import AffectedRepositoryTable from './AffectedRepositories/AffectedRepositoryTable';
import { ADDED, ALL_STATUSES, NOT_ADDED } from '../../ContentViewsConstants';
import { hasPermission } from '../../helpers';

const CVPackageGroupFilterContent = ({
  cvId, filterId, showAffectedRepos, setShowAffectedRepos, details,
}) => {
  const dispatch = useDispatch();
  const { results: filterResults } =
    useSelector(state => selectCVFilters(state, cvId), shallowEqual);
  const response = useSelector(state =>
    selectCVFilterPackageGroups(state, cvId, filterId), shallowEqual);
  const status = useSelector(state =>
    selectCVFilterPackageGroupStatus(state, cvId, filterId), shallowEqual);
  const error = useSelector(state =>
    selectCVFilterPackageGroupError(state, cvId, filterId), shallowEqual);
  const filterDetails = useSelector(state =>
    selectCVFilterDetails(state, cvId, filterId), shallowEqual);
  const { repositories = [] } = filterDetails;
  const [rows, setRows] = useState([]);
  const [searchQuery, updateSearchQuery] = useState('');
  const [activeTabKey, setActiveTabKey] = useState(0);
  const loading = status === STATUS.PENDING;
  const [bulkActionOpen, setBulkActionOpen] = useState(false);
  const [selectOpen, setSelectOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(0);
  const deselectAll = () => setRows(rows.map(row => ({ ...row, selected: false })));
  const toggleBulkAction = () => setBulkActionOpen(prevState => !prevState);
  const hasAddedSelected = rows.some(({ selected, added }) => selected && added);
  const hasNotAddedSelected = rows.some(({ selected, added }) => selected && !added);
  const { results, ...metadata } = response;
  const { permissions } = details;

  const columnHeaders = [
    __('Name'),
    __('Product'),
    __('Repository'),
    __('Description'),
    __('Status'),
  ];

  const allAddedNotAdded = [
    ALL_STATUSES,
    ADDED,
    NOT_ADDED,
  ];

  const selectedAdded = allAddedNotAdded[selectedIndex];

  const fetchItems = useCallback((params) => {
    const adjustedParams = { ...params };
    switch (selectedIndex) {
    case 0:
      adjustedParams.show_all_for = 'content_view_filter';
      adjustedParams.available_for = undefined;
      break;
    case 1:
      adjustedParams.show_all_for = undefined;
      adjustedParams.available_for = undefined;
      break;
    case 2:
      adjustedParams.show_all_for = undefined;
      adjustedParams.available_for = 'content_view_filter';
      break;
    default:
    }

    return getCVFilterPackageGroups(cvId, filterId, adjustedParams);
  }, [cvId, filterId, selectedIndex]);

  const buildRows = useCallback(() => {
    const newRows = [];
    const filterRules = filterResults.find(({ id }) => id === Number(filterId))?.rules || [];
    results.forEach((packageGroups) => {
      const {
        name,
        description,
        repository: {
          name: repositoryName,
          product: { name: productName },
        },
        filter_ids: filterIds,
        ...rest
      } = packageGroups;

      const cells = [
        { title: name },
        { title: productName },
        { title: repositoryName },
        { title: description },
        { title: <AddedStatusLabel added={filterIds.includes(parseInt(filterId, 10))} /> },
      ];

      newRows.push({
        cells,
        packagefilterId: filterRules?.find(({ uuid }) => uuid === rest.uuid)?.id,
        added: filterIds.includes(parseInt(filterId, 10)),
        ...rest,
        name,
      });
    });

    return newRows.sort(({ added: addedA }, { added: addedB }) => {
      if (addedA === addedB) return 0;
      return addedA ? -1 : 1;
    });
  }, [filterResults, filterId, results]);

  const bulkAdd = () => {
    setBulkActionOpen(false);
    const addData = rows.filter(({ selected, added }) =>
      selected && !added).map(({ uuid }) => ({ uuid }));
    dispatch(addContentViewFilterRules(filterId, addData, () =>
      dispatch(getContentViewDetails(cvId))));
    deselectAll();
  };

  const bulkRemove = () => {
    setBulkActionOpen(false);
    const packageFilterIds =
      rows.filter(({ selected, added }) =>
        selected && added).map(({ packagefilterId }) => packagefilterId);
    dispatch(deleteContentViewFilterRules(filterId, packageFilterIds, () =>
      dispatch(getContentViewDetails(cvId))));
    deselectAll();
  };

  useEffect(() => {
    if (!repositories.length && showAffectedRepos) {
      setActiveTabKey(1);
    } else {
      setActiveTabKey(0);
    }
  }, [showAffectedRepos, repositories.length]);

  useDeepCompareEffect(() => {
    if (!loading && results) {
      const newRows = buildRows();
      setRows(newRows);
    }
  }, [response, loading, buildRows, results]);

  const actionResolver = ({ added }) => [
    {
      title: __('Add'),
      isDisabled: added,
      onClick: (_event, _rowId, { uuid }) => {
        dispatch(addCVFilterRule(filterId, { uuid }, () =>
          dispatch(getContentViewDetails(cvId))));
      },
    },
    {
      title: __('Remove'),
      isDisabled: !added,
      onClick: (_event, _rowId, { packagefilterId }) => {
        dispatch(removeCVFilterRule(filterId, packagefilterId, () =>
          dispatch(getContentViewDetails(cvId))));
      },
    },
  ];

  const emptyContentTitle = __('No package groups yet');
  const emptyContentBody = __('Add repositories with package groups to content view to select them here.');
  const emptySearchTitle = __('No matching filter rules found.');
  const emptySearchBody = __('Try changing your search settings.');
  const resetFilters = () => setSelectedIndex(0);

  return (
    <Tabs
      ouiaId="cv-package-group-filter-content"
      className="margin-0-24"
      activeKey={activeTabKey}
      onSelect={(_event, eventKey) => setActiveTabKey(eventKey)}
    >
      <Tab
        ouiaId="cv-package-group-filter-content-table-tab"
        eventKey={0}
        title={<TabTitleText>{__('Package groups')}</TabTitleText>}
      >
        <div className="margin-24-0">
          <TableWrapper
            {...{
              rows,
              metadata,
              emptyContentTitle,
              emptyContentBody,
              emptySearchTitle,
              emptySearchBody,
              searchQuery,
              updateSearchQuery,
              error,
              status,
              resetFilters,
            }}
            ouiaId="content-view-package-group-filter-table"
            additionalListeners={[selectedIndex]}
            activeFilters={[selectedAdded]}
            defaultFilters={[allAddedNotAdded[0]]}
            cells={columnHeaders}
            variant={TableVariant.compact}
            autocompleteEndpoint="/katello/api/v2/package_groups"
            autocompleteQueryParams={{ filterid: filterId }}
            bookmarkController="katello_content_view_package_group_filter_rules"
            fetchItems={fetchItems}
            actionResolver={hasPermission(permissions, 'edit_content_views') ? actionResolver : null}
            onSelect={hasPermission(permissions, 'edit_content_views') ? onSelect(rows, setRows) : null}
            actionButtons={hasPermission(permissions, 'edit_content_views') &&
                status === STATUS.RESOLVED && rows.length !== 0 &&
                <Split hasGutter>
                  <SplitItem data-testid="allAddedNotAdded">
                    <Select
                      variant={SelectVariant.single}
                      onToggle={setSelectOpen}
                      ouiaId="allAddedNotAdded"
                      onSelect={(_event, selection) => {
                        setSelectedIndex(allAddedNotAdded.indexOf(selection));
                        setSelectOpen(false);
                      }}
                      selections={allAddedNotAdded[selectedIndex]}
                      isOpen={selectOpen}
                      isCheckboxSelectionBadgeHidden
                    >
                      {allAddedNotAdded.map(item =>
                        <SelectOption aria-label={item} key={item} value={item} />)}
                    </Select>
                  </SplitItem>
                  <SplitItem>
                    <Button
                      ouiaId="add-package-group-filter-rule-button"
                      isDisabled={!hasNotAddedSelected}
                      onClick={bulkAdd}
                      variant="primary"
                      aria-label="add_filter_rule"
                    >
                      {__('Add filter rule')}
                    </Button>
                  </SplitItem>
                  <SplitItem>
                    <Dropdown
                      toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />}
                      isOpen={bulkActionOpen}
                      ouiaId="cv-package-group-filter-bulk-actions-dropdown"
                      isPlain
                      dropdownItems={[
                        <DropdownItem ouiaId="bulk_remove" aria-label="bulk_remove" key="bulk_remove" isDisabled={!hasAddedSelected} component="button" onClick={bulkRemove}>
                          {__('Remove')}
                        </DropdownItem>]
                    }
                    />
                  </SplitItem>
                </Split>
            }
          />
        </div>
      </Tab>
      {(repositories.length || showAffectedRepos) &&
        <Tab
          ouiaId="affected-repos-tab"
          eventKey={1}
          title={<TabTitleText>{__('Affected repositories')}</TabTitleText>}
        >
          <div className="margin-24-0">
            <AffectedRepositoryTable cvId={cvId} filterId={filterId} repoType="yum" setShowAffectedRepos={setShowAffectedRepos} details={details} />
          </div>
        </Tab>
      }
    </Tabs>
  );
};

CVPackageGroupFilterContent.propTypes = {
  cvId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
  filterId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
  showAffectedRepos: PropTypes.bool.isRequired,
  setShowAffectedRepos: PropTypes.func.isRequired,
  details: PropTypes.shape({
    permissions: PropTypes.shape({}),
  }).isRequired,
};

export default CVPackageGroupFilterContent;