Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/__mocks__/operatorHubItemsMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const etcdPackageManifest = {
'catalog-namespace': 'openshift-operator-lifecycle-manager',
provider: 'CoreOS, Inc',
'provider-url': '',
'opsrc-provider': 'community',
},
},
spec: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,24 @@ describe('Subscribing to an Operator from Operator Hub', () => {
});
});

it('hides community Operators when "Show Community Operators" is not accepted', async() => {
expect(catalogPageView.catalogTileCount('etcd')).toBe(0);
it('shows the warning dialog when a community operator is clicked', async() => {
await(catalogPageView.catalogTileFor('etcd').click());
await operatorHubView.operatorCommunityWarningIsLoaded();
await operatorHubView.closeCommunityWarningModal();
await operatorHubView.operatorCommunityWarningIsClosed();
});

it('shows community operators when "Show Community Operators" is accepted', async() => {
await operatorHubView.showCommunityOperators();
await catalogPageView.clickFilterCheckbox('Community');
it('shows the community operator when "Show Community Operators" is accepted', async() => {
await(catalogPageView.catalogTileFor('etcd').click());
await operatorHubView.operatorCommunityWarningIsLoaded();
await operatorHubView.acceptCommunityWarningModal();
await operatorHubView.operatorCommunityWarningIsClosed();

expect(catalogPageView.catalogTileCount('etcd')).toBe(1);
expect(operatorHubView.operatorModal.isDisplayed()).toBe(true);
expect(operatorHubView.operatorModalTitle.getText()).toEqual('etcd');

await operatorHubView.closeOperatorModal();
await operatorHubView.operatorModalIsClosed();
});

it('filters Operator Hub tiles by Category', async() => {
Expand All @@ -107,6 +116,8 @@ describe('Subscribing to an Operator from Operator Hub', () => {

it('displays subscription creation form for selected Operator', async() => {
await catalogPageView.catalogTileFor('etcd').click();
await operatorHubView.operatorCommunityWarningIsLoaded();
await operatorHubView.acceptCommunityWarningModal();
await operatorHubView.operatorModalIsLoaded();
await operatorHubView.operatorModalInstallBtn.click();

Expand All @@ -123,16 +134,16 @@ describe('Subscribing to an Operator from Operator Hub', () => {
it('displays Operator as subscribed in Operator Hub', async() => {
await operatorHubView.createSubscriptionFormBtn.click();
await crudView.isLoaded();
await operatorHubView.showCommunityOperators();

expect(catalogPageView.catalogTileFor('etcd').$('.catalog-tile-pf-footer').getText()).toContain('Installed');
});

it('displays Operator in "Cluster Service Versions" view for "default" namespace', async() => {
await browser.get(`${appHost}/operatorhub/ns/${testName}`);
await crudView.isLoaded();
await operatorHubView.showCommunityOperators();
await catalogPageView.catalogTileFor('etcd').click();
await operatorHubView.operatorCommunityWarningIsLoaded();
await operatorHubView.acceptCommunityWarningModal();
await operatorHubView.operatorModalIsLoaded();
await operatorHubView.viewInstalledOperator();
await crudView.isLoaded();
Expand Down
15 changes: 8 additions & 7 deletions frontend/integration-tests/views/operator-hub.view.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-undef, no-unused-vars */

import { browser, $, ExpectedConditions as until, $$, by, element } from 'protractor';
import { browser, $, ExpectedConditions as until, by, element } from 'protractor';

export const operatorModal = $('.modal-content');
export const operatorModalIsLoaded = () => browser.wait(until.presenceOf(operatorModal), 1000)
Expand All @@ -16,9 +16,10 @@ export const createSubscriptionFormTitle = element(by.cssContainingText('h1', 'C
export const createSubscriptionFormBtn = element(by.buttonText('Subscribe'));
export const createSubscriptionFormInstallMode = element(by.cssContainingText('label', 'Installation Mode'));

export const showCommunityOperators = async() => {
await $$('.co-catalog-page__filter-toggle').click();
await browser.wait(until.presenceOf($('.co-modal-ignore-warning')), 1000).then(() => browser.sleep(500));
await $('.co-modal-ignore-warning').$('.btn-primary').click();
await browser.wait(until.not(until.presenceOf($('.co-modal-ignore-warning'))), 1000).then(() => browser.sleep(500));
};
export const communityWarningModal = $('.co-modal-ignore-warning');
export const operatorCommunityWarningIsLoaded = () => browser.wait(until.presenceOf(communityWarningModal), 1000)
.then(() => browser.sleep(500));
export const operatorCommunityWarningIsClosed = () => browser.wait(until.not(until.presenceOf(communityWarningModal)), 1000)
.then(() => browser.sleep(500));
export const closeCommunityWarningModal = () => communityWarningModal.$('.btn-default').click();
export const acceptCommunityWarningModal = () => communityWarningModal.$('.btn-primary').click();
2 changes: 2 additions & 0 deletions frontend/public/components/catalog/_catalog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ $co-modal-ignore-warning-icon-width: 30px;
}

.co-modal-ignore-warning {
height: initial;

&__checkbox.checkbox {
margin-bottom: 0;
padding-top: 15px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,23 @@ export class OperatorHubCommunityProviderModal extends React.Component<OperatorH

render() {
const { ignoreWarnings } = this.state;
const submitButtonContent = <React.Fragment>Show<span className="hidden-xs"> Community Operators</span></React.Fragment>;
const submitButtonContent = <React.Fragment>Continue</React.Fragment>;
return <form onSubmit={this.submit} className="modal-content co-modal-ignore-warning">
<ModalTitle>Show Community Operators</ModalTitle>
<ModalTitle>Show Community Operator</ModalTitle>
<ModalBody className="modal-body">
<div className="co-modal-ignore-warning__content">
<div className="co-modal-ignore-warning__icon">
<Icon type="pf" name="info" />
</div>
<div>
<p>
These are operators which have not been vetted or verified by Red Hat. Community Operators should be used with
Community Operators are operators which have not been vetted or verified by Red Hat. Community Operators should be used with
caution because their stability is unknown. Red Hat provides no support for Community Operators.
{RH_OPERATOR_SUPPORT_POLICY_LINK && (
<span className="co-modal-ignore-warning__link">
<ExternalLink href={RH_OPERATOR_SUPPORT_POLICY_LINK} text="Learn more about Red Hat’s third party software support policy" />
</span>
)}
Do you want to show Community Operators in the Operator Hub?
</p>
<Checkbox className="co-modal-ignore-warning__checkbox" onChange={this.onIgnoreChange} checked={ignoreWarnings}>
Do not show this warning again
Expand Down
148 changes: 41 additions & 107 deletions frontend/public/components/operator-hub/operator-hub-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import * as React from 'react';
import * as _ from 'lodash-es';
import * as PropTypes from 'prop-types';
import { Button, Icon, Modal } from 'patternfly-react';
import { CatalogTile, FilterSidePanel } from 'patternfly-react-extensions';
import { Icon, Modal } from 'patternfly-react';
import { CatalogTile } from 'patternfly-react-extensions';

import { history } from '../utils/router';
import { COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY } from '../../const';
import { K8sResourceKind } from '../../module/k8s';
import { requireOperatorGroup } from '../operator-lifecycle-manager/operator-group';
import { normalizeIconClass } from '../catalog/catalog-item-icon';
import { TileViewPage, updateURLParams, getFilterSearchParam, updateActiveFilters } from '../utils/tile-view-page';
import { TileViewPage } from '../utils/tile-view-page';
import { OperatorHubItemDetails } from './operator-hub-item-details';
import { communityOperatorWarningModal } from './operator-hub-community-provider-modal';

Expand All @@ -23,6 +23,9 @@ const pageDescription = (
providing a self-service experience.
</span>
);

const communityOperatorBadge = <span key="1" className="pf-c-badge pf-m-read">Community</span>;

/**
* Filter property white list
*/
Expand Down Expand Up @@ -56,7 +59,12 @@ const determineCategories = items => {
});
});

return newCategories;
const sortedKeys = _.keys(newCategories).sort((key1, key2) => key1.toLowerCase().localeCompare(key2.toLowerCase()));

return _.reduce(sortedKeys, (categories, key) => {
categories[key] = newCategories[key];
return categories;
}, {});
};

export const getProviderValue = value => {
Expand Down Expand Up @@ -189,142 +197,66 @@ const setURLParams = params => {
};

export const OperatorHubTileView = requireOperatorGroup(
// TODO: Can be functional stateless component
class OperatorHubTileView extends React.Component<OperatorHubTileViewProps, OperatorHubTileViewState> {
constructor(props) {
super(props);

this.state = {
detailsItem: null,
items: props.items,
communityOperatorsExist: false,
includeCommunityOperators: false,
showDetails: false,
communityModalShown: false,
};

this.openOverlay = this.openOverlay.bind(this);
this.closeOverlay = this.closeOverlay.bind(this);
this.renderFilterGroup = this.renderFilterGroup.bind(this);
this.renderTile = this.renderTile.bind(this);
}

componentDidMount() {
const {items} = this.props;
const searchParams = new URLSearchParams(window.location.search);
const detailsItemID = searchParams.get('details-item');
const includeCommunityOperators = searchParams.get('community-operators') === 'true';
const communityOperatorsExist = _.some(items, item => item.providerType === COMMUNITY_PROVIDER_TYPE);

let stateItems = items;
if (communityOperatorsExist && !includeCommunityOperators) {
stateItems = _.filter(items, item => item.providerType !== COMMUNITY_PROVIDER_TYPE);
}

const detailsItem = detailsItemID && _.find(stateItems, {uid: detailsItemID});
const detailsItem = detailsItemID && _.find(items, {uid: detailsItemID});

this.setState({detailsItem, items: stateItems, communityOperatorsExist, includeCommunityOperators});
this.setState({detailsItem});
}

componentDidUpdate(prevProps, prevState) {
const {items} = this.props;
const {includeCommunityOperators} = this.state;

if (!_.isEqual(items, prevProps.items) || includeCommunityOperators !== prevState.includeCommunityOperators) {
const communityOperatorsExist = _.some(items, item => item.providerType === COMMUNITY_PROVIDER_TYPE);
let stateItems = items;
if (communityOperatorsExist && !includeCommunityOperators) {
stateItems = _.filter(items, item => item.providerType !== COMMUNITY_PROVIDER_TYPE);
}
this.setState({items: stateItems, communityOperatorsExist});
}
}
showCommunityOperator = (ignoreWarning: boolean = false) => {
const { detailsItem } = this.state;

openOverlay(detailsItem) {
const params = new URLSearchParams(window.location.search);
params.set('details-item', detailsItem.uid);
setURLParams(params);
this.setState({showDetails: true});

this.setState({detailsItem});
}

closeOverlay() {
const params = new URLSearchParams(window.location.search);
params.delete('details-item');
setURLParams(params);

this.setState({detailsItem: null});
}

onIncludeCommunityOperatorsToggle = (activeFilters, onUpdateFilters) => {
const {items} = this.props;
const {includeCommunityOperators} = this.state;
if (ignoreWarning) {
localStorage.setItem(COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY, 'true');
}
};

if (!includeCommunityOperators) {
const ignoreWarning = localStorage.getItem(COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY);
if (ignoreWarning === 'true') {
this.showCommunityOperators();
return;
}
openOverlay(detailsItem) {
const ignoreWarning = localStorage.getItem(COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY) === 'true';

communityOperatorWarningModal({ showCommunityOperators: this.showCommunityOperators });
if (!ignoreWarning && detailsItem.providerType === COMMUNITY_PROVIDER_TYPE) {
this.setState({detailsItem});
communityOperatorWarningModal({ showCommunityOperators: this.showCommunityOperator });
return;
}

const stateItems = _.filter(items, item => item.providerType !== COMMUNITY_PROVIDER_TYPE);
const params = new URLSearchParams(window.location.search);
params.delete('community-operators');
params.set('details-item', detailsItem.uid);
setURLParams(params);

/* Clear the community filter if it is active */
let updatedFilters = activeFilters;
if (_.get(activeFilters, 'providerType.Community.active')) {
const groupFilter = _.cloneDeep(activeFilters.providerType);
_.set(groupFilter, [COMMUNITY_PROVIDER_TYPE, 'active'], false);
updateURLParams('providerType', getFilterSearchParam(groupFilter));
updatedFilters = updateActiveFilters(activeFilters, 'providerType', COMMUNITY_PROVIDER_TYPE, false);
onUpdateFilters(updatedFilters);
}

this.setState({items: stateItems, includeCommunityOperators: false});
};
this.setState({detailsItem, showDetails: true});
}

showCommunityOperators = (ignoreWarning: boolean = false) => {
const { items } = this.props;
closeOverlay() {
const params = new URLSearchParams(window.location.search);
params.set('community-operators', 'true');
params.delete('details-item');
setURLParams(params);

this.setState({items, includeCommunityOperators: true});

if (ignoreWarning) {
localStorage.setItem(COMMUNITY_PROVIDERS_WARNING_LOCAL_STORAGE_KEY, 'true');
}
};

renderFilterGroup(filterGroup, groupName, activeFilters, filterCounts, onFilterChange, onUpdateFilters) {
const { includeCommunityOperators, communityOperatorsExist } = this.state;
return <FilterSidePanel.Category
key={groupName}
title={operatorHubFilterMap[groupName] || groupName}
>
{_.map(filterGroup, (filter, filterName) => {
const { label, active } = filter;
return <FilterSidePanel.CategoryItem
key={filterName}
count={_.get(filterCounts, [groupName, filterName], 0)}
checked={active}
onChange={e => onFilterChange(groupName, filterName, e.target.checked)}
title={label}
>
{label}
</FilterSidePanel.CategoryItem>;
})}
{groupName === 'providerType' && communityOperatorsExist && (
<Button bsStyle="link" className="co-catalog-page__filter-toggle" onClick={() => this.onIncludeCommunityOperatorsToggle(activeFilters, onUpdateFilters)}>
{includeCommunityOperators ? 'Hide Community Operators' : 'Show Community Operators'}
</Button>
)}
</FilterSidePanel.Category>;
this.setState({detailsItem: null, showDetails: false});
}

renderTile(item) {
Expand All @@ -336,11 +268,14 @@ export const OperatorHubTileView = requireOperatorGroup(
const normalizedIconClass = iconClass && `icon ${normalizeIconClass(iconClass)}`;
const vendor = provider ? `provided by ${provider}` : null;

const badges = item.providerType === COMMUNITY_PROVIDER_TYPE ? [communityOperatorBadge] : [];

return (
<CatalogTile
id={uid}
key={uid}
title={name}
badges={badges}
iconImg={imgUrl}
iconClass={normalizedIconClass}
vendor={vendor}
Expand All @@ -352,7 +287,8 @@ export const OperatorHubTileView = requireOperatorGroup(
}

render() {
const { items, detailsItem } = this.state;
const { detailsItem, showDetails } = this.state;
const { items } = this.props;

return <React.Fragment>
<TileViewPage
Expand All @@ -361,13 +297,13 @@ export const OperatorHubTileView = requireOperatorGroup(
getAvailableCategories={determineCategories}
getAvailableFilters={determineAvailableFilters}
filterGroups={operatorHubFilterGroups}
renderFilterGroup={this.renderFilterGroup}
filterGroupNameMap={operatorHubFilterMap}
keywordCompare={keywordCompare}
renderTile={this.renderTile}
pageDescription={pageDescription}
emptyStateInfo="No Operator Hub items are being shown due to the filters being applied."
/>
<Modal show={!!detailsItem} onHide={this.closeOverlay} bsSize="lg" className="co-catalog-page__overlay right-side-modal-pf">
<Modal show={!!detailsItem && showDetails} onHide={this.closeOverlay} bsSize="lg" className="co-catalog-page__overlay right-side-modal-pf">
{detailsItem && <OperatorHubItemDetails namespace={this.props.namespace} item={detailsItem} closeOverlay={this.closeOverlay} />}
</Modal>
</React.Fragment>;
Expand All @@ -388,9 +324,7 @@ export type OperatorHubTileViewProps = {

export type OperatorHubTileViewState = {
detailsItem: any;
items: any[];
communityOperatorsExist: boolean;
includeCommunityOperators: boolean;
showDetails: boolean;
communityModalShown: boolean;
};

Expand Down
1 change: 1 addition & 0 deletions frontend/public/vendor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@
@import "~@patternfly/patternfly-next/_base";
@import '~@patternfly/patternfly-next/utilities/Accessibility/accessibility';
@import '~@patternfly/patternfly-next/utilities/Spacing/spacing';
@import '~@patternfly/patternfly-next/components/Badge/badge';