Katello/katello

View on GitHub
webpack/components/extensions/HostDetails/Tabs/ModuleStreamsTab/ModuleStreamsTab.js

Summary

Maintainability
F
1 wk
Test Coverage
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { translate as __ } from 'foremanReact/common/I18n';
import { Skeleton,
  Label,
  Button,
  Split,
  SplitItem,
  Checkbox,
  Dropdown,
  Text,
  TextVariants,
  DropdownItem,
  KebabToggle,
  DropdownPosition,
  DropdownSeparator,
  Modal,
  ModalVariant } from '@patternfly/react-core';
import PropTypes from 'prop-types';
import { upperFirst, lowerCase } from 'lodash';
import { TableText, TableVariant, Thead, Tbody, Tr, Td } from '@patternfly/react-table';
import {
  LongArrowAltUpIcon,
  CheckIcon,
} from '@patternfly/react-icons';
import { useBulkSelect, useUrlParams } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
import { useTableSort } from 'foremanReact/components/PF4/Helpers/useTableSort';
import { selectModuleStreamStatus, selectModuleStream } from './ModuleStreamsSelectors';
import { getHostModuleStreams } from './ModuleStreamsActions';
import InactiveText from '../../../../../scenes/ContentViews/components/InactiveText';
import TableWrapper from '../../../../../components/Table/TableWrapper';
import hostIdNotReady from '../../HostDetailsActions';
import { selectHostDetails } from '../../HostDetailsSelectors';
import SortableColumnHeaders from '../../../../Table/components/SortableColumnHeaders';
import SelectableDropdown from '../../../../SelectableDropdown/SelectableDropdown';
import {
  HOST_MODULE_STREAM_STATUSES, INSTALL_STATUS_PARAM_TO_FRIENDLY_NAME, INSTALLED_STATE,
  STATUS_PARAM_TO_FRIENDLY_NAME, MODULE_STREAMS_KEY,
} from './ModuleStreamsConstants';
import { moduleStreamAction } from '../RemoteExecutionActions';
import { katelloModuleStreamActionUrl } from '../customizedRexUrlHelpers';
import { useRexJobPolling } from '../RemoteExecutionHooks';
import {
  hasRequiredPermissions as can,
  missingRequiredPermissions as cannot,
  userPermissionsFromHostDetails,
} from '../../hostDetailsHelpers';

export const hideModuleStreamsTab = ({ hostDetails }) => !(hostDetails?.operatingsystem_family === 'Redhat' && Number(hostDetails?.operatingsystem_major > 7));

const EnabledIcon = ({ streamText, streamInstallStatus, upgradable }) => {
  switch (true) {
  case (streamInstallStatus?.length > 0 && streamText === 'disabled'):
    return <TableText wrapModifier="nowrap">{INSTALLED_STATE.INSTALLED}</TableText>;
  case (streamInstallStatus?.length > 0 && streamText === 'enabled' && upgradable !== true):
    return <><CheckIcon color="green" /> {INSTALLED_STATE.UPTODATE}</>;
  case (streamInstallStatus?.length > 0 && streamText === 'enabled' && upgradable):
    return <><LongArrowAltUpIcon color="blue" /> {INSTALLED_STATE.UPGRADEABLE}</>;
  default:
    return <InactiveText text={INSTALLED_STATE.NOTINSTALLED} />;
  }
};

EnabledIcon.propTypes = {
  streamText: PropTypes.string.isRequired,
  streamInstallStatus: PropTypes.arrayOf(PropTypes.string).isRequired,
  upgradable: PropTypes.bool.isRequired,
};

const stateText = (moduleStreamStatus) => {
  let streamText = moduleStreamStatus?.charAt(0)?.toUpperCase() + moduleStreamStatus?.slice(1);
  streamText = streamText?.replace('Unknown', 'Default');
  return streamText;
};

const StreamState = ({ moduleStreamStatus }) => {
  const streamText = stateText(moduleStreamStatus);
  switch (true) {
  case (streamText === 'Default'):
    return <Label color="gray" variant="outline">{streamText}</Label>;
  case (streamText === 'Disabled'):
    return <Label color="gray" variant="filled">{streamText}</Label>;
  case (streamText === 'Enabled'):
    return <Label color="green" variant="filled">{streamText}</Label>;
  default:
    return null;
  }
};

StreamState.propTypes = {
  moduleStreamStatus: PropTypes.string.isRequired,
};

const HostInstalledProfiles = ({ moduleStreamStatus, installedProfiles }) => {
  let installedProfile;
  if (installedProfiles?.length > 0) {
    installedProfile = installedProfiles?.map(profile => upperFirst(profile)).join(', ');
  } else {
    installedProfile = 'No profile installed';
  }
  const disabledText = moduleStreamStatus === 'disabled' || moduleStreamStatus === 'unknown';
  return disabledText ? <InactiveText text={installedProfile} /> : installedProfile;
};

HostInstalledProfiles.propTypes = {
  moduleStreamStatus: PropTypes.string.isRequired,
  installedProfiles: PropTypes.arrayOf(PropTypes.string).isRequired,
};

const ModuleActionConfirmationModal = ({
  hostname, action, moduleSpec, actionModalOpen, setActionModalOpen, triggerModuleStreamAction,
}) => {
  let title;
  let body;
  let confirmText;
  switch (action) {
  case 'disable':
    confirmText = __('Disable');
    title = __('Disable module stream');
    body = (
      <FormattedMessage
        id="warning-message-disable"
        defaultMessage={__('Selected module {moduleSpec} will become unavailable. \n' +
          '      The module RPMs will become unavailable in the package set.')}
        values={{
          moduleSpec,
        }}
      />
    );
    break;
  case 'reset':
    confirmText = __('Reset');
    title = __('Reset module stream');
    body = (
      <FormattedMessage
        id="warning-message-reset"
        defaultMessage={__('Selected module {moduleSpec} will be no longer enabled or disabled. \n' +
          'Consequently, all installed profiles will be removed and only RPMs from the default stream will be available in the package set.')}
        values={{
          moduleSpec,
        }}
      />
    );
    break;
  case 'remove':
    confirmText = __('Remove');
    title = __('Remove module stream');
    body = __(`Installed module profiles will be removed. Additionally, all packages whose names are provided by specific modules will be removed.
      Packages required by other installed modules profiles and packages whose names are also provided by other modules are not removed.`);
    break;
  default:
    // No action selected. Should not be here!
    setActionModalOpen(false);
  }
  return (
    <Modal
      variant={ModalVariant.small}
      isOpen={actionModalOpen}
      aria-label="Module action confirmation modal"
      ouiaId="Module-action-confirmation-modal"
      title={title}
      titleIconVariant="warning"
      showClose
      onClose={() => setActionModalOpen(false)}
      actions={[
        <Button
          aria-label="confirm-module-action"
          ouiaId="confirm-module-action"
          key="confirm-module-action"
          onClick={() => {
            triggerModuleStreamAction({ hostname, action, moduleSpec });
            setActionModalOpen(false);
          }}
        >
          {confirmText}
        </Button>,
        <Button
          aria-label="cancel-module-action"
          ouiaId="cancel-module-action"
          key="cancel-module-action"
          variant="link"
          onClick={() => setActionModalOpen(false)}
        >
          {__('Cancel')}
        </Button>,
      ]}
    >
      <Text component={TextVariants.p} ouiaId="text">
        {body}
      </Text>

    </Modal>
  );
};

ModuleActionConfirmationModal.propTypes = {
  hostname: PropTypes.string.isRequired,
  action: PropTypes.string.isRequired,
  moduleSpec: PropTypes.string.isRequired,
  actionModalOpen: PropTypes.bool.isRequired,
  setActionModalOpen: PropTypes.func.isRequired,
  triggerModuleStreamAction: PropTypes.func.isRequired,
};

const invokeRexJobs = ['create_job_invocations'];
const createBookmarks = ['create_bookmarks'];

export const ModuleStreamsTab = () => {
  const hostDetails = useSelector(selectHostDetails);
  const { id: hostId, name: hostname } = hostDetails;
  const showActions = can(invokeRexJobs, userPermissionsFromHostDetails({ hostDetails }));
  const [useCustomizedRex, setUseCustomizedRex] = useState('');
  const [dropdownOpen, setDropdownOpen] = useState('');
  const [actionModalOpen, setActionModalOpen] = useState(false);
  const [actionableModuleSpec, setActionableModuleSpec] = useState(null);
  const [hostModuleStreamAction, setHostModuleStreamAction] = useState(null);

  const emptyContentTitle = __('This host does not have any Module streams.');
  const emptyContentBody = __('Module streams will appear here after enabling Red Hat repositories or creating custom products.');
  const emptySearchTitle = __('Your search returned no matching Module streams.');
  const emptySearchBody = __('Try changing your search criteria.');
  const showPrimaryAction = true;
  const showSecondaryAction = true;
  const primaryActionTitle = __('Enable Red Hat repositories');
  const secondaryActionTitle = __('Create a custom product');
  const primaryActionLink = '/redhat_repositories';
  const secondaryActionLink = '/products/new';
  const errorSearchTitle = __('Problem searching module streams');
  const {
    status: initialStatus,
    installStatus: initialInstallStatusSelected,
    searchParam,
  } = useUrlParams();
  const MODULE_STREAM_STATUS = __('Status');
  const MODULE_STREAM_INSTALLATION_STATUS = __('Installation status');
  const [statusSelected, setStatusSelected] =
    useState(STATUS_PARAM_TO_FRIENDLY_NAME[initialStatus] ??
    MODULE_STREAM_STATUS);
  const [installStatusSelected, setInstallStatusSelected] =
    useState(INSTALL_STATUS_PARAM_TO_FRIENDLY_NAME[initialInstallStatusSelected] ??
    MODULE_STREAM_INSTALLATION_STATUS);
  const columnHeaders = [
    __('Name'),
    __('State'),
    __('Stream'),
    __('Installation status'),
    __('Installed profile'),
  ];
  const COLUMNS_TO_SORT_PARAMS = {
    [columnHeaders[0]]: 'name',
    [columnHeaders[1]]: 'status',
    [columnHeaders[3]]: 'installed_profiles',
  };

  const {
    pfSortParams, apiSortParams,
    activeSortColumn, activeSortDirection,
  } = useTableSort({
    allColumns: columnHeaders,
    columnsToSortParams: COLUMNS_TO_SORT_PARAMS,
    initialSortColumnName: 'Name',
  });

  const {
    triggerJobStart: triggerModuleStreamAction, lastCompletedJob: tableJobCompleted,
    isPolling: isModuleStreamActionInProgress,
  } = useRexJobPolling(moduleStreamAction);

  const {
    triggerJobStart: triggerConfirmModalAction, lastCompletedJob: confirmModalJobCompleted,
    isPolling: isConfirmModalActionInProgress,
  } = useRexJobPolling(moduleStreamAction);

  const actionInProgress = (isModuleStreamActionInProgress || isConfirmModalActionInProgress);

  const fetchItems = useCallback(
    (params) => {
      let extraParams = params;
      if (!hostId) return hostIdNotReady;
      if (statusSelected !== MODULE_STREAM_STATUS) {
        extraParams = { ...extraParams, status: lowerCase(statusSelected) };
      }
      if (installStatusSelected !== MODULE_STREAM_INSTALLATION_STATUS) {
        extraParams = { ...extraParams, install_status: lowerCase(installStatusSelected) };
      }
      return getHostModuleStreams(
        hostId,
        { ...apiSortParams, ...extraParams },
      );
    },
    [hostId, statusSelected, installStatusSelected,
      MODULE_STREAM_STATUS, MODULE_STREAM_INSTALLATION_STATUS, apiSortParams],
  );

  const handleModuleStreamStatusSelected = newStatus => setStatusSelected((prevStatus) => {
    if (prevStatus === newStatus) {
      return MODULE_STREAM_STATUS;
    }
    return newStatus;
  });

  const handleModuleStreamInstallationStatusSelected =
      newInstallationStatus => setInstallStatusSelected((prevInstallationStatus) => {
        if (prevInstallationStatus === newInstallationStatus) {
          return MODULE_STREAM_INSTALLATION_STATUS;
        }
        return newInstallationStatus;
      });

  const customizedActionURL = (action, moduleSpec) =>
    katelloModuleStreamActionUrl({ hostname, action, moduleSpec });

  const response = useSelector(selectModuleStream);
  const { results, ...metadata } = response;
  const { error: errorSearchBody } = metadata;
  const status = useSelector(state => selectModuleStreamStatus(state));
  /* eslint-disable no-unused-vars */
  const {
    selectOne, isSelected, searchQuery, selectedCount, isSelectable,
    updateSearchQuery, selectNone, fetchBulkParams, ...selectAll
  } = useBulkSelect({
    results,
    metadata,
    isSelectable: _result => false,
    initialSearchQuery: searchParam || '',
  });
  /* eslint-enable no-unused-vars */

  const hideBookmarkActions =
    cannot(createBookmarks, userPermissionsFromHostDetails({ hostDetails }));

  if (!hostId) return <Skeleton />;

  const activeFilters = [statusSelected, installStatusSelected];
  const defaultFilters = [MODULE_STREAM_STATUS, MODULE_STREAM_INSTALLATION_STATUS];

  const resetFilters = () => {
    setStatusSelected(MODULE_STREAM_STATUS);
    setInstallStatusSelected(MODULE_STREAM_INSTALLATION_STATUS);
  };

  return (
    <div>
      <div id="modulestreams-tab">
        <TableWrapper
          {...{
            metadata,
            emptyContentTitle,
            emptyContentBody,
            emptySearchTitle,
            emptySearchBody,
            errorSearchTitle,
            errorSearchBody,
            searchQuery,
            updateSearchQuery,
            fetchItems,
            activeFilters,
            defaultFilters,
            status,
            showPrimaryAction,
            showSecondaryAction,
            primaryActionTitle,
            secondaryActionTitle,
            primaryActionLink,
            secondaryActionLink,
            resetFilters,
          }}
          ouiaId="host-module-stream-table"
          additionalListeners={[hostId, activeSortColumn, activeSortDirection,
            statusSelected, installStatusSelected, confirmModalJobCompleted,
            tableJobCompleted]}
          fetchItems={fetchItems}
          bookmarkController="katello_host_available_module_streams"
          readOnlyBookmarks={hideBookmarkActions}
          autocompleteEndpoint={`/api/v2/hosts/${hostId}/module_streams`}
          rowsCount={results?.length}
          variant={TableVariant.compact}
          requestKey={MODULE_STREAMS_KEY}
          alwaysShowActionButtons={false}
          actionButtons={
            <Split hasGutter>
              <SplitItem>
                <SelectableDropdown
                  id="status-dropdown"
                  title={MODULE_STREAM_STATUS}
                  showTitle={false}
                  items={Object.values(HOST_MODULE_STREAM_STATUSES)}
                  selected={statusSelected}
                  setSelected={handleModuleStreamStatusSelected}
                />
              </SplitItem>
              <SplitItem>
                <SelectableDropdown
                  id="install-status-dropdown"
                  title={MODULE_STREAM_INSTALLATION_STATUS}
                  showTitle={false}
                  items={Object.values(INSTALLED_STATE)}
                  selected={installStatusSelected}
                  setSelected={handleModuleStreamInstallationStatusSelected}
                />
              </SplitItem>
            </Split>
          }
        >
          <Thead>
            <Tr ouiaId="header-tr">
              <SortableColumnHeaders
                columnHeaders={columnHeaders}
                pfSortParams={pfSortParams}
                columnsToSortParams={COLUMNS_TO_SORT_PARAMS}
              />
            </Tr>
          </Thead>
          <Tbody>
            {results?.map(({
              id,
              status: moduleStreamStatus,
              name,
              stream,
              installed_profiles: installedProfiles,
              upgradable,
              install_status: installedStatus,
              module_spec: moduleSpec,
            }, index) => {
              /* eslint-disable react/no-array-index-key */
              const dropdownItems = [
                <DropdownItem key={`dropdownItem-checkbox-${id}`} ouiaId={`dropdownItem-checkbox-${id}`}>
                  <Checkbox
                    aria-label={`customize-checkbox-${id}`}
                    ouiaId={`customize-checkbox-${id}`}
                    id={`Checkbox${id}`}
                    label={__('Customize with Rex')}
                    isChecked={id === useCustomizedRex}
                    onChange={checked => (checked ? setUseCustomizedRex(id) : setUseCustomizedRex(''))}
                  />
                </DropdownItem>,
                <DropdownSeparator key={`separator-${id}`} ouiaId={`separator-${id}`} />,

              ];
              if (id === useCustomizedRex) {
                dropdownItems.push(
                  <DropdownItem
                    aria-label={`enable-${id}-href`}
                    ouiaId={`enable-${id}-href`}
                    key={`dropdownItem-enable-url-${id}`}
                    component="a"
                    href={customizedActionURL('enable', moduleSpec)}
                    isDisabled={stateText(moduleStreamStatus) ===
                    HOST_MODULE_STREAM_STATUSES.ENABLED}
                  >
                    {__('Enable')}
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`disable-${id}-href`}
                    ouiaId={`disable-${id}-href`}
                    key={`dropdownItem-disable-url-${id}`}
                    component="a"
                    href={customizedActionURL('disable', moduleSpec)}
                    isDisabled={stateText(moduleStreamStatus) !==
                    HOST_MODULE_STREAM_STATUSES.ENABLED}
                  >
                    {__('Disable')}
                    <InactiveText style={{ marginBottom: '1px' }} text={__('Prevent from further updates')} />
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`install-${id}-href`}
                    ouiaId={`install-${id}-href`}
                    key={`dropdownItem-install-url-${id}`}
                    component="a"
                    href={customizedActionURL('install', moduleSpec)}
                    isDisabled={(upgradable ||
                      (installedStatus !== INSTALLED_STATE.NOTINSTALLED) ||
                      !(stateText(moduleStreamStatus) === HOST_MODULE_STREAM_STATUSES.ENABLED ||
                        stateText(moduleStreamStatus) === HOST_MODULE_STREAM_STATUSES.DISABLED)
                    )}
                  >
                    {__('Install')}
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`update-${id}-href`}
                    ouiaId={`update-${id}-href`}
                    key={`dropdownItem-update-${id}`}
                    component="a"
                    href={customizedActionURL('update', moduleSpec)}
                    isDisabled={!upgradable}
                  >
                    {__('Update')}
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`reset-${id}-href`}
                    ouiaId={`reset-${id}-href`}
                    key={`dropdownItem-reset-${id}`}
                    component="a"
                    href={customizedActionURL('reset', moduleSpec)}
                  >
                    {__('Reset')}
                    <InactiveText style={{ marginBottom: '1px' }} text={__('Reset to the default state')} />
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`remove-${id}-href`}
                    ouiaId={`remove-${id}-href`}
                    key={`dropdownItem-remove-${id}`}
                    component="a"
                    href={customizedActionURL('remove', moduleSpec)}
                  >
                    {__('Remove')}
                    <InactiveText style={{ marginBottom: '1px' }} text={__('Uninstall and reset')} />
                  </DropdownItem>,
                );
              } else {
                dropdownItems.push(
                  <DropdownItem
                    aria-label={`enable-${id}-button`}
                    ouiaId={`enable-${id}-button`}
                    key={`dropdownItem-enable-${id}`}
                    component="button"
                    onClick={() => {
                      triggerModuleStreamAction({ hostname, action: 'enable', moduleSpec });
                      setUseCustomizedRex('');
                      setDropdownOpen('');
                    }}
                    isDisabled={actionInProgress || stateText(moduleStreamStatus) ===
                    HOST_MODULE_STREAM_STATUSES.ENABLED}
                  >
                    {__('Enable')}
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`disable-${id}-button`}
                    ouiaId={`disable-${id}-button`}
                    key={`dropdownItem-disable-${id}`}
                    component="button"
                    onClick={() => {
                      setActionableModuleSpec(moduleSpec);
                      setHostModuleStreamAction('disable');
                      setActionModalOpen(true);
                      setUseCustomizedRex('');
                      setDropdownOpen('');
                    }}
                    isDisabled={actionInProgress || stateText(moduleStreamStatus) !==
                    HOST_MODULE_STREAM_STATUSES.ENABLED}
                  >
                    {__('Disable')}
                    <InactiveText style={{ marginBottom: '1px' }} text={__('Prevent from further updates')} />
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`install-${id}-button`}
                    ouiaId={`install-${id}-button`}
                    key={`dropdownItem-install-${id}`}
                    component="button"
                    onClick={() => {
                      triggerModuleStreamAction({ hostname, action: 'install', moduleSpec });
                      setUseCustomizedRex('');
                      setDropdownOpen('');
                    }}
                    isDisabled={(actionInProgress || upgradable ||
                      (installedStatus !== INSTALLED_STATE.NOTINSTALLED) ||
                      !(stateText(moduleStreamStatus) === HOST_MODULE_STREAM_STATUSES.ENABLED ||
                        stateText(moduleStreamStatus) === HOST_MODULE_STREAM_STATUSES.DISABLED)
                    )}
                  >
                    {__('Install')}
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`update-${id}-button`}
                    ouiaId={`update-${id}-button`}
                    key={`dropdownItem-update-${id}`}
                    component="button"
                    onClick={() => {
                      triggerModuleStreamAction({ hostname, action: 'update', moduleSpec });
                      setUseCustomizedRex('');
                      setDropdownOpen('');
                    }}
                    isDisabled={actionInProgress || !upgradable}
                  >
                    {__('Update')}
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`reset-${id}-button`}
                    ouiaId={`reset-${id}-button`}
                    key={`dropdownItem-reset-${id}`}
                    component="button"
                    onClick={() => {
                      setActionableModuleSpec(moduleSpec);
                      setHostModuleStreamAction('reset');
                      setActionModalOpen(true);
                      setUseCustomizedRex('');
                      setDropdownOpen('');
                    }}
                    isDisabled={actionInProgress}
                  >
                    {__('Reset')}
                    <InactiveText style={{ marginBottom: '1px' }} text={__('Reset to the default state')} />
                  </DropdownItem>,
                  <DropdownItem
                    aria-label={`remove-${id}-button`}
                    ouiaId={`remove-${id}-button`}
                    key={`dropdownItem-remove-${id}`}
                    component="button"
                    onClick={() => {
                      setActionableModuleSpec(moduleSpec);
                      setHostModuleStreamAction('remove');
                      setActionModalOpen(true);
                      setUseCustomizedRex('');
                      setDropdownOpen('');
                    }}
                    isDisabled={actionInProgress}
                  >
                    {__('Remove')}
                    <InactiveText style={{ marginBottom: '1px' }} text={__('Uninstall and reset')} />
                  </DropdownItem>,
                );
              }
              return (
                <Tr key={`${id} ${index}`} ouiaId={`tr-${id}-${index}`}>
                  <Td>
                    <a
                      href={`/module_streams?search=module_spec%3D${moduleSpec}+and+host%3D${hostname}`}
                    >
                      {name}
                    </a>
                  </Td>
                  <Td>
                    <StreamState moduleStreamStatus={moduleStreamStatus} />
                  </Td>
                  <Td>{stream}</Td>
                  <Td>
                    <EnabledIcon
                      streamText={moduleStreamStatus}
                      streamInstallStatus={installedProfiles}
                      upgradable={upgradable}
                    />
                  </Td>
                  <Td>
                    <HostInstalledProfiles
                      moduleStreamStatus={moduleStreamStatus}
                      installedProfiles={installedProfiles}
                    />
                  </Td>
                  {showActions && (
                    <Td key={`actions-td-${id}-${dropdownOpen}`}>
                      <Dropdown
                        aria-label={`actions-dropdown-${id}`}
                        ouiaId={`actions-dropdown-${id}`}
                        key={`actions-dropdown-${id}-${dropdownOpen}`}
                        isPlain
                        style={{ width: 'inherit' }}
                        position={DropdownPosition.right}
                        toggle={
                          <KebabToggle aria-label={`kebab-dropdown-${id}`} onToggle={() => ((dropdownOpen === id) ? setDropdownOpen('') : setDropdownOpen(id))} id={`toggle-dropdown-${id}`} />
                      }
                        isOpen={id === dropdownOpen}
                        dropdownItems={dropdownItems}
                      />
                    </Td>
                  )}
                </Tr>
              );
            })
            }
          </Tbody>
        </TableWrapper>
        {actionModalOpen &&
          <ModuleActionConfirmationModal
            hostname={hostname}
            action={hostModuleStreamAction}
            moduleSpec={actionableModuleSpec}
            actionModalOpen={actionModalOpen}
            setActionModalOpen={setActionModalOpen}
            triggerModuleStreamAction={triggerConfirmModalAction}
          />
        }
      </div>
    </div>
  );
};

export default ModuleStreamsTab;