Skip to content

Commit

Permalink
Fixes #34169 - Support bulk removing versions (#9899)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrewgdewar committed Mar 14, 2022
1 parent b65c9e4 commit 197688c
Show file tree
Hide file tree
Showing 28 changed files with 4,891 additions and 208 deletions.
17 changes: 9 additions & 8 deletions webpack/components/Table/TableHooks.js
Expand Up @@ -61,7 +61,8 @@ export const useSet = (initialArry) => {
};

export const useSelectionSet = ({
results, metadata,
results,
metadata,
initialArry = [],
idColumn = 'id',
isSelectable = () => true,
Expand All @@ -77,7 +78,7 @@ export const useSelectionSet = ({
}, [idColumn, selectableResults]);
const areAllRowsOnPageSelected = () =>
Number(pageIds?.length) > 0 &&
pageIds.every(result => selectionSet.has(result) || !canSelect(result));
pageIds.every(result => selectionSet.has(result) || !canSelect(result));

const areAllRowsSelected = () =>
Number(selectionSet.size) > 0 && selectionSet.size === Number(metadata.selectable);
Expand Down Expand Up @@ -146,20 +147,20 @@ export const useBulkSelect = ({
isSelectable,
}) => {
const { selectionSet: inclusionSet, ...selectOptions } =
useSelectionSet({
results, metadata, initialArry, idColumn, isSelectable,
});
useSelectionSet({
results, metadata, initialArry, idColumn, isSelectable,
});
const exclusionSet = useSet([]);
const [searchQuery, updateSearchQuery] = useState(initialSearchQuery);
const [selectAllMode, setSelectAllMode] = useState(false);
const selectedCount = selectAllMode ?
Number(metadata.selectable) - exclusionSet.size : selectOptions.selectedCount;

const areAllRowsOnPageSelected = () => selectAllMode ||
selectOptions.areAllRowsOnPageSelected();
selectOptions.areAllRowsOnPageSelected();

const areAllRowsSelected = () => (selectAllMode && exclusionSet.size === 0) ||
selectOptions.areAllRowsSelected();
selectOptions.areAllRowsSelected();

const isSelected = useCallback((id) => {
if (!selectOptions.isSelectable(id)) {
Expand Down Expand Up @@ -225,7 +226,7 @@ export const useBulkSelect = ({
// if search value changed and cleared from a string to empty value
// And it was select all -> then reset selections
if ((prevSearchRef && !isEmpty(prevSearchRef.searchQuery)) &&
isEmpty(searchQuery) && selectAllMode) {
isEmpty(searchQuery) && selectAllMode) {
selectNone();
}
}, [searchQuery, selectAllMode, prevSearchRef, selectNone]);
Expand Down
Expand Up @@ -309,7 +309,6 @@ export const ErrataTab = () => {
isSelected={toggleGroupState === ALL}
onChange={() => setToggleGroupState(ALL)}
/>

<ToggleGroupItem
text={__('Installable')}
buttonId="installableToggle"
Expand Down
11 changes: 11 additions & 0 deletions webpack/global_test_setup.js
@@ -1,4 +1,6 @@
// runs before each test to make sure console.error output will
import nock from 'nock';

// fail a test (i.e. default PropType missing). Check the error
// output and traceback for actual error.
global.console.error = (error, stack) => {
Expand All @@ -14,3 +16,12 @@ afterAll(() => {
jest.resetModules();
if (global.gc) global.gc();
});

beforeEach(() => {
if (!nock.isActive()) { nock.activate(); }
});

afterEach(() => {
nock.restore();
});

1 change: 1 addition & 0 deletions webpack/scenes/ContentViews/ContentViewsConstants.js
Expand Up @@ -43,6 +43,7 @@ export const cvDetailsHistoryKey = cvId => `${CONTENT_VIEWS_KEY}_HISTORIES_${cvI
export const cvFilterRulesKey = filterId => `CONTENT_VIEW_FILTER_${filterId}_RULES`;
export const cvDetailsComponentKey = cvId => `${CONTENT_VIEWS_KEY}_COMPONENTS_${cvId}`;
export const cvDetailsVersionKey = cvId => `${CONTENT_VIEWS_KEY}_VERSIONS_${cvId}`;
export const bulkRemoveVersionKey = cvId => `BULK_REMOVE_CV_VERSION_${cvId}`;
export const cvRemoveVersionKey = (versionId, versionEnvironments) => `REMOVE_CV_VERSION_${versionId}_${versionEnvironments.length}`;
export const cvVersionPromoteKey = (versionId, environmentIds) => `PROMOTE_CONTENT_VIEW_VERSION_${versionId}_${environmentIds.length}`;
export const cvVersionDetailsKey = (cvId, versionId) => `CONTENT_VIEW_VERSION_DETAILS_${cvId}_${versionId}`;
Expand Down
13 changes: 13 additions & 0 deletions webpack/scenes/ContentViews/Details/ContentViewDetailActions.js
Expand Up @@ -48,6 +48,7 @@ import {
DOCKER_TAGS_CONTENT,
generatedContentKey,
STATUS_TRANSLATIONS_ENUM,
bulkRemoveVersionKey,
} from '../ContentViewsConstants';
import api, { foremanApi, orgId } from '../../../services/api';
import { getResponseErrorMsgs } from '../../../utils/helpers';
Expand Down Expand Up @@ -414,6 +415,18 @@ export const editContentViewVersionDetails = (versionId, cvId, params, handleSuc
errorToast: error => __(`Something went wrong while editing version details. ${getResponseErrorMsgs(error.response)}`),
});

export const bulkDeleteContentViewVersion = (cvId, params, handleSuccess) => put({
type: API_OPERATIONS.PUT,
key: bulkRemoveVersionKey(cvId),
url: api.getApiUrl(`/content_views/${cvId}/bulk_delete_versions`),
params,
handleSuccess: (response) => {
renderTaskStartedToast(response?.data);
return handleSuccess();
},
errorToast: error => __(`Something went wrong while deleting versions ${getResponseErrorMsgs(error.response)}`),
});

export const removeContentViewVersion = (cvId, versionId, versionEnvironments, params) => put({
type: API_OPERATIONS.PUT,
key: cvRemoveVersionKey(versionId, versionEnvironments),
Expand Down
56 changes: 33 additions & 23 deletions webpack/scenes/ContentViews/Details/ContentViewDetailSelectors.js
@@ -1,42 +1,43 @@
import { STATUS } from 'foremanReact/constants';
import {
selectAPIStatus,
selectAPIError,
selectAPIResponse,
selectAPIStatus,
} from 'foremanReact/redux/API/APISelectors';
import { STATUS } from 'foremanReact/constants';
import { pollTaskKey } from '../../Tasks/helpers';
import {
ACTIVATION_KEY_KEY,
ADD_CONTENT_VIEW_FILTER_RULE,
bulkRemoveVersionKey,
CREATE_CONTENT_VIEW_FILTER_KEY,
cvAddComponentKey,
cvDetailsComponentKey,
cvDetailsFiltersKey,
cvDetailsHistoryKey,
cvDetailsKey,
cvDetailsRepoKey,
cvDetailsFiltersKey,
cvDetailsVersionKey,
cvFilterDetailsKey,
cvFilterPackageGroupsKey,
cvFilterModuleStreamKey,
cvFilterErratumIDKey,
cvDetailsHistoryKey,
cvFilterModuleStreamKey,
cvFilterPackageGroupsKey,
cvFilterRepoKey,
cvFilterRulesKey,
cvDetailsVersionKey,
REPOSITORY_TYPES,
cvDetailsComponentKey,
cvAddComponentKey,
cvRemoveComponentKey,
CREATE_CONTENT_VIEW_FILTER_KEY,
ADD_CONTENT_VIEW_FILTER_RULE,
ACTIVATION_KEY_KEY,
HOSTS_KEY,
cvFilterRepoKey,
cvVersionDetailsKey,
cvRemoveVersionKey,
RPM_PACKAGES_CONTENT,
RPM_PACKAGE_GROUPS_CONTENT,
REPOSITORY_CONTENT,
FILE_CONTENT,
ERRATA_CONTENT,
MODULE_STREAMS_CONTENT,
cvVersionDetailsKey,
DEB_PACKAGES_CONTENT,
DOCKER_TAGS_CONTENT,
ERRATA_CONTENT,
FILE_CONTENT,
generatedContentKey,
HOSTS_KEY,
MODULE_STREAMS_CONTENT,
REPOSITORY_CONTENT,
REPOSITORY_TYPES,
RPM_PACKAGE_GROUPS_CONTENT,
RPM_PACKAGES_CONTENT,
} from '../ContentViewsConstants';
import { pollTaskKey } from '../../Tasks/helpers';

export const selectCVDetails = (state, cvId) =>
selectAPIResponse(state, cvDetailsKey(cvId)) || {};
Expand Down Expand Up @@ -275,6 +276,15 @@ export const selectCVHosts = state =>
export const selectCVHostsStatus = state =>
selectAPIStatus(state, HOSTS_KEY) || STATUS.PENDING;

export const selectBulkRemoveCVVersionResponse = (state, cvId) =>
selectAPIResponse(state, bulkRemoveVersionKey(cvId)) || {};

export const selectBulkRemoveCVVersionStatus = (state, cvId) =>
selectAPIStatus(state, bulkRemoveVersionKey(cvId)) || STATUS.PENDING;

export const selectBulkRemoveCVVersionError = (state, cvId) =>
selectAPIError(state, bulkRemoveVersionKey(cvId));

export const selectRemoveCVVersionResponse = (state, versionId, versionEnvironments) =>
selectAPIResponse(state, cvRemoveVersionKey(versionId, versionEnvironments)) || {};

Expand Down
@@ -0,0 +1,58 @@
import React from 'react';
import { PropTypes } from 'prop-types';
import {
Flex,
FlexItem,
Label,
Text,
} from '@patternfly/react-core';
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
import {
global_warning_color_100 as warningColor,
} from '@patternfly/react-tokens';

const ActionSummary = ({ title, text, selectedEnv: { name, id } }) => (
<div>
{title &&
<h3 style={{ margin: '8px 0' }}><b>{title}</b></h3>
}
{text &&
<Flex>
<FlexItem style={{ marginRight: '8px' }}>
<ExclamationTriangleIcon color={warningColor.value} />
</FlexItem>
<FlexItem style={{ marginRight: '8px' }}>
<Text>{text}</Text>
</FlexItem>
{name && id &&
<FlexItem>
<Label isTruncated color="purple" href={`/lifecycle_environments/${id}`}>{name}</Label>
</FlexItem>
}
</Flex>
}
</div>);


ActionSummary.propTypes = {
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object, // React component
]),
text: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object, // React component
]),
selectedEnv: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
}),
};

ActionSummary.defaultProps = {
title: undefined,
text: undefined,
selectedEnv: {},
};

export default ActionSummary;
@@ -0,0 +1,45 @@
import React, {
createContext,
useState,
} from 'react';

import { PropTypes } from 'prop-types';

export const BulkDeleteContext = createContext({});

const BulkDeleteContextWrapper = ({
children, versions, onClose,
}) => {
const [selectedEnvForAK, setSelectedEnvForAK] = useState([]);
const [selectedCVForAK, setSelectedCVForAK] = useState(null);
const [selectedEnvForHosts, setSelectedEnvForHosts] = useState([]);
const [selectedCVForHosts, setSelectedCVForHosts] = useState(null);
const [currentStep, setCurrentStep] = useState(1);

return (
<BulkDeleteContext.Provider value={{
onClose,
versions,
selectedEnvForAK,
setSelectedEnvForAK,
selectedCVForAK,
setSelectedCVForAK,
selectedEnvForHosts,
setSelectedEnvForHosts,
selectedCVForHosts,
setSelectedCVForHosts,
currentStep,
setCurrentStep,
}}
>
{children}
</BulkDeleteContext.Provider>);
};

BulkDeleteContextWrapper.propTypes = {
versions: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
onClose: PropTypes.func.isRequired,
children: PropTypes.element.isRequired,
};

export default BulkDeleteContextWrapper;
@@ -0,0 +1,30 @@
import { translate as __ } from 'foremanReact/common/I18n';
import { sum } from 'lodash';

export const getNumberOfActivationKeys = versions =>
sum(versions.map(({ environments }) =>
sum(environments.map(({ activation_key_count: akCount }) => akCount))));

export const getNumberOfHosts = versions =>
sum(versions.map(({ environments }) =>
sum(environments.map(({ host_count: hostCount }) => hostCount))));

// Gets a non-duplicated list of environments from within a given set of versions
export const getEnvironmentList = (versions) => {
const envIds = [];
const environmentList = [];
versions.forEach(({ environments }) => environments.forEach((env) => {
if (!envIds.includes(env.id)) {
environmentList.push(env);
envIds.push(env.id);
}
}));
return environmentList;
};

export const getNumberOfEnvironments = versions => getEnvironmentList(versions).length;

// Creates a string from a list of versions: '3.0' or '3.0 and 2.0' or '3.0, 2.0 and 1.0' etc.
export const getVersionListString = versions => versions.map(({ version }, index) =>
`${index > 0 && index === (versions.length - 1) ?
__(' and') : ''} ${version}${versions.length - index > 2 ? ',' : ''}`).join('');
@@ -0,0 +1,56 @@
import React, { useContext } from 'react';

import { translate as __ } from 'foremanReact/common/I18n';
import { PropTypes } from 'prop-types';
import { FormattedMessage } from 'react-intl';

import { Wizard } from '@patternfly/react-core';

import BulkDeleteContextWrapper, {
BulkDeleteContext,
} from './BulkDeleteContextWrapper';
import {
getVersionListString,
} from './BulkDeleteHelpers';
import bulkDeleteSteps from './bulkDeleteSteps';

const BulkDeleteModal = ({ versions, onClose }) => {
const WizardWithContext = () => {
const context = useContext(BulkDeleteContext);
const versionList = getVersionListString(versions);
const description =
(<FormattedMessage
id="bulk-delete-modal-title"
values={{ versionList }}
defaultMessage={versions.length === 1 ?
__('Deleting version {versionList}') :
__('Deleting versions: {versionList}')}
/>);

return (
<Wizard
title={versions.length === 1 ?
__('Delete version') :
__('Delete versions')}
description={description}
steps={bulkDeleteSteps(context)}
onGoToStep={({ id }) => context.setCurrentStep(id)}
onNext={({ id }) => context.setCurrentStep(id)}
onBack={({ id }) => context.setCurrentStep(id)}
onClose={onClose}
isOpen
/>);
};

return (
<BulkDeleteContextWrapper {...{ versions, onClose }}>
<WizardWithContext />
</BulkDeleteContextWrapper>
);
};

BulkDeleteModal.propTypes = {
versions: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
onClose: PropTypes.func.isRequired,
};
export default BulkDeleteModal;

0 comments on commit 197688c

Please sign in to comment.