Katello/katello

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

Summary

Maintainability
F
5 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, Dropdown, DropdownItem, KebabToggle, Button } 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 {
  selectCVFilterDetails,
  selectCVFilterRules,
  selectCVFilterRulesStatus,
} from '../ContentViewDetailSelectors';
import { deleteContentViewFilterRules, getCVFilterRules, removeCVFilterRule } from '../ContentViewDetailActions';
import CVRpmMatchContentModal from './MatchContentModal/CVRpmMatchContentModal';
import AddEditPackageRuleModal from './Rules/Package/AddEditPackageRuleModal';
import AffectedRepositoryTable from './AffectedRepositories/AffectedRepositoryTable';
import { hasPermission } from '../../helpers';
import { emptyContentTitle,
  emptyContentBody,
  emptySearchTitle,
  emptySearchBody } from './FilterRuleConstants';
import { CONTENT_VIEW_NEEDS_PUBLISH } from '../../ContentViewsConstants';

const CVRpmFilterContent = ({
  cvId, filterId, inclusion, showAffectedRepos, setShowAffectedRepos, details,
}) => {
  const response = useSelector(state => selectCVFilterRules(state, filterId), shallowEqual);
  const { results, ...metadata } = response;
  const status = useSelector(state => selectCVFilterRulesStatus(state, filterId), shallowEqual);
  const loading = status === STATUS.PENDING;
  const filterDetails = useSelector(state =>
    selectCVFilterDetails(state, cvId, filterId), shallowEqual);
  const { repositories = [] } = filterDetails;
  const dispatch = useDispatch();

  const [rows, setRows] = useState([]);
  const [searchQuery, updateSearchQuery] = useState('');
  const [activeTabKey, setActiveTabKey] = useState(0);
  const [filterRuleId, setFilterRuleId] = useState(undefined);
  const [bulkActionOpen, setBulkActionOpen] = useState(false);
  const deselectAll = () => setRows(rows.map(row => ({ ...row, selected: false })));
  const toggleBulkAction = () => setBulkActionOpen(prevState => !prevState);
  const hasSelected = rows.some(({ selected }) => selected);
  const [modalOpen, setModalOpen] = useState(false);
  const [selectedFilterRuleData, setSelectedFilterRuleData] = useState(undefined);
  const [showMatchContent, setShowMatchContent] = useState(false);
  const { permissions } = details;

  const onClose = () => {
    setModalOpen(false);
    setShowMatchContent(false);
    setSelectedFilterRuleData(undefined);
  };

  const columnHeaders = [
    __('RPM name'),
    __('Architecture'),
    __('Versions'),
  ];

  const versionText = (rule) => {
    const { version, min_version: minVersion, max_version: maxVersion } = rule;

    if (rule.version) return `Version ${version}`;
    if (rule.min_version && !rule.max_version) return `Greater than version ${minVersion}`;
    if (!rule.min_version && rule.max_version) return `Less than version ${maxVersion}`;
    if (rule.min_version && rule.max_version) {
      return `Between versions ${rule.min_version} and ${rule.max_version}`;
    }
    return 'All versions';
  };

  const buildRows = useCallback(() => {
    const newRows = [];
    results.forEach((rule) => {
      const {
        name, architecture, id, ...rest
      } = rule;

      const cells = [
        { title: name },
        { title: architecture || 'All architectures' },
        { title: versionText(rule) },
      ];

      newRows.push({
        cells, id, name, arch: architecture, ...rest,
      });
    });

    return newRows;
  }, [results]);

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

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

  const tabTitle = (inclusion ? __('Included') : __('Excluded')) + __(' RPMs');


  const actionResolver = () => [
    {
      title: __('Remove'),
      onClick: (_event, _rowId, { id }) => {
        dispatch(removeCVFilterRule(filterId, id, () => {
          dispatch(getCVFilterRules(filterId));
          dispatch({ type: CONTENT_VIEW_NEEDS_PUBLISH });
        }));
      },
    },
    {
      title: __('Edit'),
      onClick: (_event, _rowId, ruleDetails) => {
        setSelectedFilterRuleData(ruleDetails);
        setModalOpen(true);
      },
    },
    {
      title: __('View matching content'),
      onClick: (_event, _rowId, { id }) => {
        setFilterRuleId(id);
        setShowMatchContent(true);
      },
    },
  ];

  const bulkRemove = () => {
    setBulkActionOpen(false);
    const rpmFilterIds =
      rows.filter(row => row.selected).map(selected => selected.id);
    dispatch(deleteContentViewFilterRules(filterId, rpmFilterIds, () => {
      dispatch(getCVFilterRules(filterId));
      dispatch({ type: CONTENT_VIEW_NEEDS_PUBLISH });
    }));
    deselectAll();
  };

  const showPrimaryAction = true;
  const primaryActionButton =
    (
      <Button
        ouiaId="add-rpm-rule-button"
        onClick={() => setModalOpen(true)}
        variant="primary"
        aria-label="add_rpm_rule_empty_state"
      > {__('Add RPM rule')}
      </Button>);

  return (
    <Tabs
      className="margin-0-24"
      ouiaId="cv-rpm-filter-content-tabs"
      activeKey={activeTabKey}
      onSelect={(_event, eventKey) => setActiveTabKey(eventKey)}
    >
      <Tab
        ouiaId="cv-rpm-filter-content-table-tab"
        eventKey={0}
        title={<TabTitleText>{tabTitle}</TabTitleText>}
      >
        <div className="margin-24-0">
          <TableWrapper
            {...{
              rows,
              metadata,
              emptyContentTitle,
              emptyContentBody,
              emptySearchTitle,
              emptySearchBody,
              searchQuery,
              updateSearchQuery,
              status,
              showPrimaryAction,
              primaryActionButton,
            }}
            ouiaId="content-view-rpm-filter-table"
            actionResolver={hasPermission(permissions, 'edit_content_views') ? actionResolver : null}
            onSelect={hasPermission(permissions, 'edit_content_views') ? onSelect(rows, setRows) : null}
            cells={columnHeaders}
            variant={TableVariant.compact}
            autocompleteEndpoint={`/katello/api/v2/content_view_filters/${filterId}/rules`}
            bookmarkController="katello_content_view_package_filter_rules"
            fetchItems={useCallback(params => getCVFilterRules(filterId, params), [filterId])}
            actionButtons={
              <>
                {showMatchContent &&
                  <CVRpmMatchContentModal
                    key={`${filterId}-${filterRuleId}`}
                    filterRuleId={filterRuleId}
                    filterId={filterId}
                    onClose={onClose}
                  />}
                {hasPermission(permissions, 'edit_content_views') &&
                  status === STATUS.RESOLVED && rows.length !== 0 &&
                  <Split hasGutter>
                    <SplitItem>
                      <Button
                        ouiaId="add-rpm-rule-button"
                        onClick={() => setModalOpen(true)}
                        variant="primary"
                        aria-label="add_rpm_rule"
                      >
                        {__('Add RPM rule')}
                      </Button>
                    </SplitItem>
                    <SplitItem>
                      <Dropdown
                        ouiaId="rpm-filter-content-bulk-actions"
                        toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />}
                        isOpen={bulkActionOpen}
                        isPlain
                        dropdownItems={[
                          <DropdownItem ouiaId="bulk-remove" aria-label="bulk_remove" key="bulk_remove" isDisabled={!hasSelected} component="button" onClick={bulkRemove}>
                            {__('Remove')}
                          </DropdownItem>]
                        }
                      />
                    </SplitItem>
                  </Split>}
                {modalOpen &&
                  <AddEditPackageRuleModal
                    filterId={filterId}
                    onClose={onClose}
                    selectedFilterRuleData={selectedFilterRuleData}
                    repositoryIds={details.repository_ids}
                  />}
              </>}
          />
        </div>
      </Tab>
      {(repositories.length || showAffectedRepos) &&
        <Tab
          ouiaId="cv-rpm-filter-content-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>
  );
};

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

CVRpmFilterContent.defaultProps = {
  cvId: '',
  filterId: '',
  inclusion: false,
};
export default CVRpmFilterContent;