src: Remove SmartComponents/PresentationalComponents split

Just have a directory per component.
This commit is contained in:
Sanne Raymaekers 2021-04-20 16:15:47 +02:00
parent 01963bf877
commit 1b9cf6df7b
29 changed files with 15 additions and 15 deletions

View file

@ -0,0 +1,365 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { actions } from '../../store/actions';
import { Button, Text, Wizard } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import WizardStepImageOutput from './WizardStepImageOutput';
import WizardStepUploadAWS from './WizardStepUploadAWS';
import WizardStepUploadAzure from './WizardStepUploadAzure';
import WizardStepPackages from './WizardStepPackages';
import WizardStepUploadGoogle from './WizardStepUploadGoogle';
import WizardStepRegistration from './WizardStepRegistration';
import WizardStepReview from './WizardStepReview';
import ImageWizardFooter from './ImageWizardFooter';
import './CreateImageWizard.scss';
class CreateImageWizard extends Component {
constructor(props) {
super(props);
this.onStep = this.onStep.bind(this);
this.onSave = this.onSave.bind(this);
this.onClose = this.onClose.bind(this);
this.validate = this.validate.bind(this);
this.validateUploadAmazon = this.validateUploadAmazon.bind(this);
this.state = {
/* errors take form of $fieldId: error */
uploadAWSErrors: {},
uploadAzureErrors: {},
uploadGoogleErrors: {},
isSaveInProgress: false,
isValidSubscription: true,
};
}
async componentDidMount() {
let user = await insights.chrome.auth.getUser();
this.setState({
subscription: {
organization: Number(user.identity.internal.org_id)
}
});
}
onStep(step) {
if (step.name === 'Review') {
this.validate();
}
}
validate() {
/* upload */
Object.keys(this.props.uploadDestinations).forEach(provider => {
switch (provider) {
case 'aws':
this.validateUploadAmazon();
break;
case 'azure':
this.validateUploadAzure();
break;
case 'google':
break;
default:
break;
}
});
/* subscription */
if (this.props.subscribeNow) {
this.setState({ isValidSubscription: this.props.subscription.activationKey ? true : false });
} else {
this.setState({ isValidSubscription: true });
}
}
validateUploadAmazon() {
let uploadAWSErrors = {};
let share = this.props.uploadAWS.shareWithAccounts;
if (share.length === 0 || share[0].length !== 12 || isNaN(share[0])) {
uploadAWSErrors['aws-account-id'] =
{ label: 'AWS account ID', value: 'A 12-digit number is required' };
}
this.setState({ uploadAWSErrors });
}
validateUploadAzure() {
let uploadAzureErrors = {};
let tenant_id = this.props.uploadAzure.tenantId;
if (tenant_id === null || tenant_id === '') {
uploadAzureErrors['azure-resource-group'] =
{ label: 'Azure tenant ID', value: 'A tenant ID is required' };
}
let subscriptionId = this.props.uploadAzure.subscriptionId;
if (subscriptionId === null || subscriptionId === '') {
uploadAzureErrors['azure-subscription-id'] =
{ label: 'Azure subscription ID', value: 'A subscription ID is required' };
}
let resource_group = this.props.uploadAzure.resourceGroup;
if (resource_group === null || resource_group === '') {
uploadAzureErrors['azure-resource-group'] =
{ label: 'Azure resource group', value: 'A resource group is required' };
}
// TODO check oauth2 thing too here?
}
onSave() {
this.setState({ isSaveInProgress: true });
let customizations = {
packages: this.props.selectedPackages,
};
if (this.props.subscribeNow) {
customizations.subscription = {
'activation-key': this.props.subscription.activationKey,
insights: this.props.subscription.insights,
organization: this.props.subscription.organization,
'server-url': 'subscription.rhsm.redhat.com',
'base-url': 'https://cdn.redhat.com/',
};
}
let requests = [];
if (this.props.uploadDestinations.aws) {
let request = {
distribution: this.props.release.distro,
image_requests: [
{
architecture: this.props.release.arch,
image_type: 'ami',
upload_request: {
type: 'aws',
options: {
share_with_accounts: this.props.uploadAWS.shareWithAccounts,
},
},
}],
customizations,
};
requests.push(request);
}
if (this.props.uploadDestinations.google) {
let share = '';
switch (this.props.uploadGoogle.accountType) {
case 'googleAccount':
share = 'user:' + this.props.uploadGoogle.shareWithAccounts[0].user;
break;
case 'serviceAccount':
share = 'serviceAccount:' + this.props.uploadGoogle.shareWithAccounts[0].serviceAccount;
break;
case 'googleGroup':
share = 'group:' + this.props.uploadGoogle.shareWithAccounts[0].group;
break;
case 'domain':
share = 'domain:' + this.props.uploadGoogle.shareWithAccounts[0].domain;
break;
}
let request = {
distribution: this.props.release.distro,
image_requests: [
{
architecture: this.props.release.arch,
image_type: 'vhd',
upload_request: {
type: 'gcp',
options: {
share_with_accounts: [ share ],
},
},
}],
customizations,
};
requests.push(request);
}
if (this.props.uploadDestinations.azure) {
let request = {
distribution: this.props.release.distro,
image_requests: [
{
architecture: this.props.release.arch,
image_type: 'vhd',
upload_request: {
type: 'azure',
options: {
tenant_id: this.props.uploadAzure.tenantId,
subscription_id: this.props.uploadAzure.subscriptionId,
resource_group: this.props.uploadAzure.resourceGroup,
},
},
}],
customizations,
};
requests.push(request);
}
const composeRequests = requests.map(request => this.props.composeStart(request));
Promise.all(composeRequests)
.then(() => {
if (!this.props.composesError) {
this.props.addNotification({
variant: 'success',
title: 'Your image is being created',
});
this.props.history.push('/landing');
}
this.setState({ isSaveInProgress: false });
});
}
onClose () {
this.props.history.push('/landing');
}
render() {
const isValidUploadDestination = this.props.uploadDestinations.aws ||
this.props.uploadDestinations.azure ||
this.props.uploadDestinations.google;
const StepImageOutput = {
name: 'Image output',
component: <WizardStepImageOutput />
};
const StepUploadAWS = {
name: 'Amazon Web Services',
component: <WizardStepUploadAWS
errors={ this.state.uploadAWSErrors } />
};
const StepUploadAzure = {
name: 'Microsoft Azure',
component: <WizardStepUploadAzure
errors={ this.state.uploadAzureErrors } />
};
const StepUploadGoogle = {
name: 'Google Cloud Platform',
component: <WizardStepUploadGoogle
errors={ this.state.uploadGoogleErrors } />
};
const uploadDestinationSteps = [];
if (this.props.uploadDestinations.aws) {
uploadDestinationSteps.push(StepUploadAWS);
}
if (this.props.uploadDestinations.azure) {
uploadDestinationSteps.push(StepUploadAzure);
}
if (this.props.uploadDestinations.google) {
uploadDestinationSteps.push(StepUploadGoogle);
}
const StepTargetEnv = {
name: 'Target environment',
steps: uploadDestinationSteps
};
const steps = [
StepImageOutput,
...(StepTargetEnv.steps.length > 0 ? [ StepTargetEnv ] : []),
{
name: 'Registration',
component: <WizardStepRegistration
isValidSubscription={ this.state.isValidSubscription } /> },
{
name: 'Packages',
component: <WizardStepPackages /> },
{
name: 'Review',
component: <WizardStepReview
uploadAWSErrors={ this.state.uploadAWSErrors }
isValidSubscription={ this.state.isValidSubscription } />,
nextButtonText: 'Create',
}
];
return (
<React.Fragment>
<Wizard
title={ 'Create image' }
description={ <Text>
Create a RHEL image and push it to cloud providers.
{' '}
<Button
component="a"
target="_blank"
variant="link"
icon={ <ExternalLinkAltIcon /> }
iconPosition="right"
isInline
href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8-beta/">
Documentation
</Button>
</Text> }
onNext={ this.onStep }
onGoToStep={ this.onStep }
steps={ steps }
onClose={ this.onClose }
onSave={ this.onSave }
footer={ <ImageWizardFooter
isValidUploadDestination={ isValidUploadDestination }
isSaveInProgress={ this.state.isSaveInProgress }
isValidSubscription={ this.state.isValidSubscription }
error={ this.props.composesError } /> }
isOpen />
</React.Fragment>
);
}
}
function mapStateToProps(state) {
return {
composesError: state.composes.error,
release: state.pendingCompose.release,
uploadDestinations: state.pendingCompose.uploadDestinations,
uploadAWS: state.pendingCompose.uploadAWS,
uploadAzure: state.pendingCompose.uploadAzure,
uploadGoogle: state.pendingCompose.uploadGoogle,
selectedPackages: state.pendingCompose.selectedPackages,
subscription: state.pendingCompose.subscription,
subscribeNow: state.pendingCompose.subscribeNow,
};
}
function mapDispatchToProps(dispatch) {
return {
composeUpdated: (compose) => dispatch(actions.composeUpdated(compose)),
composeStart: (composeRequest) => dispatch(actions.composeStart(composeRequest)),
addNotification: (not) => dispatch(addNotification(not)),
};
}
CreateImageWizard.propTypes = {
composesError: PropTypes.string,
composeUpdated: PropTypes.func,
composeStart: PropTypes.func,
addNotification: PropTypes.func,
history: PropTypes.object,
release: PropTypes.object,
uploadDestinations: PropTypes.object,
uploadAWS: PropTypes.object,
uploadAzure: PropTypes.object,
uploadGoogle: PropTypes.object,
subscription: PropTypes.object,
subscribeNow: PropTypes.bool,
selectedPackages: PropTypes.array,
};
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(CreateImageWizard));

View file

@ -0,0 +1,3 @@
.pf-c-wizard__nav-list {
padding-right: 0px;
}

View file

@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonVariant, Text, TextContent, WizardContextConsumer, WizardFooter } from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import './ImageWizardFooter.scss';
const ImageWizardFooter = (props) => {
return (
<>
<WizardFooter>
<WizardContextConsumer>
{({ activeStep, onNext, onBack, onClose }) => {
let nextButtonText = 'Next';
if (activeStep.name === 'Review') {
nextButtonText = props.isSaveInProgress ? 'Creating...' : 'Create';
}
let nextButtonIsDisabled = props.isSaveInProgress;
if ((activeStep.name === 'Image output' || activeStep.name === 'Review') && !props.isValidUploadDestination) {
nextButtonIsDisabled = true;
}
if ((activeStep.name === 'Registration' || activeStep.name === 'Review') && !props.isValidSubscription) {
nextButtonIsDisabled = true;
}
return (
<>
<Button aria-label={ activeStep.name === 'Review' ? 'Create' : 'Next' } variant={ ButtonVariant.primary }
onClick={ onNext } isDisabled={ nextButtonIsDisabled }>
{ nextButtonText }
</Button>
<Button aria-label="Back" variant={ ButtonVariant.secondary }
onClick={ onBack } isDisabled={ props.isSaveInProgress || activeStep.name === 'Image output' }>
Back
</Button>
<Button aria-label="Cancel" variant={ ButtonVariant.link }
onClick={ onClose } isDisabled={ props.isSaveInProgress }>
Cancel
</Button>
</>
);
}}
</WizardContextConsumer>
{ props.error && (
<TextContent className="footer-error">
<Text><ExclamationCircleIcon /> <strong>{props.error}</strong></Text>
</TextContent>
)}
</WizardFooter>
</>
);
};
ImageWizardFooter.propTypes = {
isValidUploadDestination: PropTypes.bool,
isSaveInProgress: PropTypes.bool,
isValidSubscription: PropTypes.bool,
error: PropTypes.string,
};
export default ImageWizardFooter;

View file

@ -0,0 +1,4 @@
.footer-error {
flex-basis: 100%;
color: var(--pf-global--palette--red-100);
}

View file

@ -0,0 +1,109 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { actions } from '../../store/actions';
import { Form, FormGroup, FormSelect, FormSelectOption, Tile, Title } from '@patternfly/react-core';
import './WizardStepImageOutput.scss';
class WizardStepImageOutput extends Component {
constructor(props) {
super(props);
this.setDistro = this.setDistro.bind(this);
this.toggleUploadDestination = this.toggleUploadDestination.bind(this);
}
setDistro(distro) {
this.props.setRelease({ arch: 'x86_64', distro });
}
toggleUploadDestination(provider) {
this.props.setUploadDestinations({
...this.props.uploadDestinations,
[provider]: !this.props.uploadDestinations[provider]
});
}
render() {
const releaseOptions = [
{ value: 'rhel-8', label: 'Red Hat Enterprise Linux (RHEL) 8.3' },
{ value: 'centos-8', label: 'CentOS Stream 8' },
];
return (
<>
<Form>
<Title headingLevel="h2" size="xl">Image output</Title>
<FormGroup isRequired label="Release" fieldId="release-select">
<FormSelect value={ this.props.release.distro } onChange={ value => this.setDistro(value) } isRequired
aria-label="Select release input" id="release-select" data-testid="release-select">
{ releaseOptions.map(option => <FormSelectOption key={ option.value } value={ option.value } label={ option.label } />) }
</FormSelect>
</FormGroup>
<FormGroup isRequired label="Select target environment" data-testid="target-select">
<div className="tiles">
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-aws"
title="Amazon Web Services"
icon={ <img
className='provider-icon'
src={ '/apps/frontend-assets/partners-icons/aws.svg' } /> }
onClick={ () => this.toggleUploadDestination('aws') }
isSelected={ this.props.uploadDestinations.aws }
isStacked
isDisplayLarge />
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-azure"
title="Microsoft Azure"
icon={ <img
className='provider-icon'
src={ '/apps/frontend-assets/partners-icons/microsoft-azure-short.svg' } /> }
onClick={ () => this.toggleUploadDestination('azure') }
isSelected={ this.props.uploadDestinations.azure }
isStacked
isDisplayLarge />
<Tile
className="tile"
data-testid="upload-google"
title="Google Cloud Platform"
icon={ <img
className='provider-icon'
src={ '/apps/frontend-assets/partners-icons/google-cloud-short.svg' } /> }
onClick={ () => this.toggleUploadDestination('google') }
isSelected={ this.props.uploadDestinations.google }
isStacked
isDisplayLarge />
</div>
</FormGroup>
</Form>
</>
);
}
};
function mapStateToProps(state) {
return {
release: state.pendingCompose.release,
uploadDestinations: state.pendingCompose.uploadDestinations,
};
}
function mapDispatchToProps(dispatch) {
return {
setRelease: i => dispatch(actions.setRelease(i)),
setUploadDestinations: d => dispatch(actions.setUploadDestinations(d)),
};
}
WizardStepImageOutput.propTypes = {
setRelease: PropTypes.func,
setUploadDestinations: PropTypes.func,
release: PropTypes.object,
uploadDestinations: PropTypes.object,
};
export default connect(mapStateToProps, mapDispatchToProps)(WizardStepImageOutput);

View file

@ -0,0 +1,26 @@
.tiles {
display: flex;
}
.tile {
flex: 1 0 0px;
}
.pf-c-tile:focus {
--pf-c-tile__title--Color: var(--pf-c-tile__title--Color);
--pf-c-tile__icon--Color: var(---pf-global--Color--100);
--pf-c-tile--before--BorderWidth: var(--pf-global--BorderWidth--sm);
--pf-c-tile--before--BorderColor: var(--pf-global--BorderColor--100);
}
.pf-c-tile.pf-m-selected:focus {
--pf-c-tile__title--Color: var(--pf-c-tile--focus__title--Color);
--pf-c-tile__icon--Color: var(--pf-c-tile--focus__icon--Color);
--pf-c-tile--before--BorderWidth: var(--pf-c-tile--focus--before--BorderWidth);
--pf-c-tile--before--BorderColor: var(--pf-c-tile--focus--before--BorderColor);
}
.provider-icon {
width: 1em;
height: 1em;
}

View file

@ -0,0 +1,129 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button, DualListSelector, Text, TextContent, Title } from '@patternfly/react-core';
import { actions } from '../../store/actions';
import api from '../../api.js';
class WizardStepPackages extends Component {
constructor(props) {
super(props);
this.setPackagesSearchName = this.setPackagesSearchName.bind(this);
this.handlePackagesSearch = this.handlePackagesSearch.bind(this);
this.handlePackagesFilter = this.handlePackagesFilter.bind(this);
this.packageListChange = this.packageListChange.bind(this);
this.mapPackagesToComponent = this.mapPackagesToComponent.bind(this);
this.state = {
packagesAvailableComponents: [],
packagesSelectedComponents: [],
packagesFilteredComponents: [],
packagesSelectedNames: [],
packagesSearchName: '',
};
}
setPackagesSearchName(packagesSearchName) {
this.setState({ packagesSearchName });
}
mapPackagesToComponent(packages) {
return packages.map((pack) =>
<TextContent key={ pack }>
<span className="pf-c-dual-list-selector__item-text">{ pack.name }</span>
<small>{ pack.summary }</small>
</TextContent>
);
}
// this digs into the component properties to extract the package name
mapComponentToPackageName(component) {
return component.props.children[0].props.children;
}
handlePackagesSearch() {
api.getPackages(this.props.release.distro, this.props.release.arch, this.state.packagesSearchName).then(response => {
const packageComponents = this.mapPackagesToComponent(response.data);
this.setState({
packagesAvailableComponents: packageComponents
});
});
};
handlePackagesFilter(filter) {
const filteredPackages = this.state.packagesSelectedComponents.filter(component => {
const name = this.mapComponentToPackageName(component);
return name.includes(filter);
});
this.setState({
packagesFilteredComponents: filteredPackages
});
}
packageListChange(newAvailablePackages, newChosenPackages) {
const chosenNames = newChosenPackages.map(component => this.mapComponentToPackageName(component));
this.setState({
packagesAvailableComponents: newAvailablePackages,
packagesSelectedComponents: newChosenPackages,
packagesFilteredComponents: newChosenPackages,
});
this.props.setSelectedPackages(chosenNames);
}
render() {
const availableOptionsActions = [
<Button
aria-label="Search button for available packages"
key="availableSearchButton"
onClick={ this.handlePackagesSearch }>
Search
</Button>
];
return (
<>
<TextContent>
<Title headingLevel="h2" size="xl">Additional packages</Title>
<Text>Optionally add additional packages to your <strong>{this.props.release.distro}</strong> image</Text>
</TextContent>
<DualListSelector
className="pf-u-mt-sm"
isSearchable
availableOptionsActions={ availableOptionsActions }
availableOptions={ this.state.packagesAvailableComponents }
chosenOptions={ this.state.packagesFilteredComponents }
addSelected={ this.packageListChange }
removeSelected={ this.packageListChange }
addAll={ this.packageListChange }
removeAll= { this.packageListChange }
onAvailableOptionsSearchInputChanged={ this.setPackagesSearchName }
onChosenOptionsSearchInputChanged={ this.handlePackagesFilter }
filterOption={ () => true }
id="basicSelectorWithSearch" />
</>
);
}
};
function mapStateToProps(state) {
return {
release: state.pendingCompose.release,
selectedPackages: state.pendingCompose.selectedPackages,
};
}
function mapDispatchToProps(dispatch) {
return {
setSelectedPackages: (p) => dispatch(actions.setSelectedPackages(p)),
};
}
WizardStepPackages.propTypes = {
release: PropTypes.object,
selectedPackages: PropTypes.array,
setSelectedPackages: PropTypes.func,
};
export default connect(mapStateToProps, mapDispatchToProps)(WizardStepPackages);

View file

@ -0,0 +1,81 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Form, FormGroup, TextInput, Radio, Title } from '@patternfly/react-core';
import { actions } from '../../store/actions';
class WizardStepRegistration extends Component {
constructor(props) {
super(props);
}
async componentDidMount() {
let user = await insights.chrome.auth.getUser();
this.props.setSubscription(Object.assign(this.props.subscription, { organization: user.identity.internal.org_id }));
}
render() {
return (
<Form>
<Title headingLevel="h2" size="xl">Registration</Title>
<FormGroup isRequired label="Register the system">
<Radio name="subscribe-now-radio" isChecked={ this.props.subscribeNow } id="subscribe-now-radio"
label="Embed an activation key and register systems on first boot"
onChange={ () => this.props.setSubscribeNow(true) }
data-testid="register-now-radio-button" />
<Radio name="subscribe-later-radio" isChecked={ !this.props.subscribeNow }
label="Register the system later" id="subscribe-later-radio"
onChange={ () => this.props.setSubscribeNow(false) }
data-testid="register-later-radio-button" />
</FormGroup>
{ this.props.subscribeNow &&
<>
<FormGroup label="Organization ID" fieldId="subscription-organization">
<TextInput isDisabled value={ this.props.subscription.organization || '' } type="text"
id="subscription-organization" aria-label="Subscription organization ID"
data-testid="organization-id" />
</FormGroup>
<FormGroup isRequired label="Activation key" fieldId="subscription-activation"
helperTextInvalid={ 'A value is required' }
validated={ !this.props.isValidSubscription && this.props.subscription.activationKey !== null ? 'error' : 'default' }>
<TextInput
value={ this.props.subscription.activationKey || '' }
type="password"
data-testid="subscription-activation"
id="subscription-activation"
aria-label="Subscription activation key"
onChange={ activationKey => this.props.setSubscription(Object.assign(this.props.subscription, { activationKey })) }
validated={ !this.props.isValidSubscription && this.props.subscription.activationKey !== null ? 'error' : 'default' }
isRequired />
</FormGroup>
</> }
</Form>
);
}
};
function mapStateToProps(state) {
return {
subscription: state.pendingCompose.subscription,
subscribeNow: state.pendingCompose.subscribeNow,
};
}
function mapDispatchToProps(dispatch) {
return {
setSubscription: s => dispatch(actions.setSubscription(s)),
setSubscribeNow: s => dispatch(actions.setSubscribeNow(s)),
};
}
WizardStepRegistration.propTypes = {
setSubscription: PropTypes.func,
setSubscribeNow: PropTypes.func,
subscription: PropTypes.object,
subscribeNow: PropTypes.bool,
isValidSubscription: PropTypes.bool,
};
export default connect(mapStateToProps, mapDispatchToProps)(WizardStepRegistration);

View file

@ -0,0 +1,155 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
Alert,
Text, TextVariants, TextContent, TextList, TextListVariants, TextListItem, TextListItemVariants,
Title
} from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import './WizardStepReview.scss';
class WizardStepReview extends Component {
constructor(props) {
super(props);
}
render() {
const releaseLabels = {
'rhel-8': 'Red Hat Enterprise Linux (RHEL) 8.3',
'centos-8': 'CentOS Stream 8'
};
const awsReview = (
<>
<Text id="destination-header">Amazon Web Services</Text>
<TextList component={ TextListVariants.dl } data-testid='review-image-upload-aws'>
<TextListItem component={ TextListItemVariants.dt }>Account ID</TextListItem>
{this.props.uploadAWSErrors['aws-account-id'] ? (
<TextListItem component={ TextListItemVariants.dd }>
<ExclamationCircleIcon className="error" /> { this.props.uploadAWSErrors['aws-account-id'].value }
</TextListItem>
) : (
<TextListItem component={ TextListItemVariants.dd }>{this.props.uploadAWS.shareWithAccounts[0]}</TextListItem>
)}
</TextList>
</>
);
const googleReview = (
<>
<Text id="destination-header">Google Cloud Platform</Text>
<TextList component={ TextListVariants.dl } data-testid='review-image-upload-google'>
{this.props.uploadGoogle.accountType === 'googleAccount' && (
<>
<TextListItem component={ TextListItemVariants.dt }>Google account</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>{this.props.uploadGoogle.shareWithAccounts[0] ?
this.props.uploadGoogle.shareWithAccounts[0].user || '' :
''}
</TextListItem>
</>
)}
{this.props.uploadGoogle.accountType === 'serviceAccount' && (
<>
<TextListItem component={ TextListItemVariants.dt }>Service account</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>{this.props.uploadGoogle.shareWithAccounts[0] ?
this.props.uploadGoogle.shareWithAccounts[0].serviceAccount || '' :
''}
</TextListItem>
</>
)}
{this.props.uploadGoogle.accountType === 'googleGroup' && (
<>
<TextListItem component={ TextListItemVariants.dt }>Google group</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>{this.props.uploadGoogle.shareWithAccounts[0] ?
this.props.uploadGoogle.shareWithAccounts[0].group || '' :
''}
</TextListItem>
</>
)}
{this.props.uploadGoogle.accountType === 'domain' && (
<>
<TextListItem component={ TextListItemVariants.dt }>Domain</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>{this.props.uploadGoogle.shareWithAccounts[0] ?
this.props.uploadGoogle.shareWithAccounts[0].domain || '' :
''}
</TextListItem>
</>
)}
</TextList>
</>
);
let subscriptionReview = <TextListItem component={ TextListItemVariants.dd }>Register the system later</TextListItem>;
if (this.props.subscribeNow) {
subscriptionReview = (<>
<TextListItem component={ TextListItemVariants.dd }>Register the system on first boot</TextListItem>
<TextListItem component={ TextListItemVariants.dt }>Activation key</TextListItem>
{ !this.props.isValidSubscription || !this.props.subscription.activationKey ? (
<TextListItem component={ TextListItemVariants.dd }>
<ExclamationCircleIcon className="error" /> { 'A value is required' }
</TextListItem>
) : (
<TextListItem component={ TextListItemVariants.dd } type="password">
{'*'.repeat(this.props.subscription.activationKey.length)}
</TextListItem>
)}
</>);
}
return (
<>
{ (Object.keys(this.props.uploadAWSErrors).length > 0 ||
!this.props.isValidSubscription) &&
<Alert variant="danger" className="pf-u-mb-xl" isInline title="Required information is missing" /> }
<Title headingLevel="h2" size="xl">Review</Title>
<TextContent>
<Text component={ TextVariants.small }>
Review the information and click Create image
to create the image using the following criteria.
</Text>
<Text component={ TextVariants.h3 }>Image output</Text>
<TextList component={ TextListVariants.dl } data-testid='review-image-output'>
<TextListItem component={ TextListItemVariants.dt }>Release</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>{releaseLabels[this.props.release]}</TextListItem>
</TextList>
<Text component={ TextVariants.h3 }>Target environment</Text>
{this.props.uploadDestinations.aws && awsReview }
{this.props.uploadDestinations.google && googleReview }
<Text component={ TextVariants.h3 }>Registration</Text>
<TextList component={ TextListVariants.dl } data-testid='review-image-registration'>
<TextListItem component={ TextListItemVariants.dt }>Subscription</TextListItem>
{ subscriptionReview }
</TextList>
</TextContent>
</>
);
}
};
function mapStateToProps(state) {
return {
uploadDestinations: state.pendingCompose.uploadDestinations,
uploadAWS: state.pendingCompose.uploadAWS,
uploadAzure: state.pendingCompose.uploadAzure,
uploadGoogle: state.pendingCompose.uploadGoogle,
subscribeNow: state.pendingCompose.subscribeNow,
subscription: state.pendingCompose.subscription,
};
}
WizardStepReview.propTypes = {
release: PropTypes.string,
uploadAWS: PropTypes.object,
uploadGoogle: PropTypes.object,
uploadDestinations: PropTypes.object,
uploadAzure: PropTypes.object,
subscription: PropTypes.object,
subscribeNow: PropTypes.bool,
uploadAWSErrors: PropTypes.object,
isValidSubscription: PropTypes.bool,
};
export default connect(mapStateToProps, null)(WizardStepReview);

View file

@ -0,0 +1,21 @@
.error {
color: var(--pf-global--danger-color--100);
}
// Increasing margins for h3 for better spacing and readability
.textcontent-review h3 {
margin-top: var(--pf-global--spacer--xl);
}
// Decreasing gap between dl items for better spacing and readability
// Also setting a first column width to 25% instead of auto,
// to guarantee the same width for each section.
@media screen and (min-width: 576px) {
.textcontent-review dl {
grid-template: 1fr / 25% 1fr;
grid-gap: var(--pf-global--spacer--sm);
}
}
#destination-header {
font-size: 18px;
margin-bottom: var(--pf-global--spacer--sm);
}

View file

@ -0,0 +1,54 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { actions } from '../../store/actions';
import { Form, FormGroup, TextInput, Title } from '@patternfly/react-core';
class WizardStepUploadAWS extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Form>
<Title headingLevel="h2" size="xl">Target Environment - Upload to AWS</Title>
<p>
Your image will be uploaded to a temporary account on Amazon Web Services. <br />
The image will be shared with the account you provide below. <br />
Within the next 14 days you will need to copy the shared image to your own account.
After 14 days it will be unavailable and will have to be regenerated.
</p>
<FormGroup isRequired label="AWS account ID" fieldId="aws-account-id"
helperTextInvalid={ (this.props.errors['aws-account-id'] && this.props.errors['aws-account-id'].value) || '' }
validated={ (this.props.errors['aws-account-id'] && 'error') || 'default' }>
<TextInput value={ this.props.uploadAWS.shareWithAccounts || '' }
type="text" aria-label="AWS account ID" id="aws-account-id"
data-testid="aws-account-id" isRequired
onChange={ value => this.props.setUploadAWS({ shareWithAccounts: [ value ]}) } />
</FormGroup>
</Form>
);
}
};
function mapStateToProps(state) {
return {
uploadAWS: state.pendingCompose.uploadAWS,
};
}
function mapDispatchToProps(dispatch) {
return {
setUploadAWS: u => dispatch(actions.setUploadAWS(u)),
};
}
WizardStepUploadAWS.propTypes = {
setUploadAWS: PropTypes.func,
uploadAWS: PropTypes.object,
errors: PropTypes.object,
};
export default connect(mapStateToProps, mapDispatchToProps)(WizardStepUploadAWS);

View file

@ -0,0 +1,98 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { actions } from '../../store/actions';
import { Form, FormGroup, Text, TextContent, TextInput, Title } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import './WizardStepUploadAzure.scss';
class WizardStepUploadAzure extends Component {
constructor(props) {
super(props);
}
render() {
return (
<>
<TextContent className="textcontent-azure">
<Title headingLevel="h2">Target Environment - Upload to Azure</Title>
<Text>
Image Builder will send an image to an authorized Azure account.
</Text>
<Title headingLevel="h3">OAuth permissions</Title>
<Text>
In order to use Image Builder to push images to Azure, Image Builder must
be configured as an authorized application, and given the role of &quot;Contributor&quot; to at least one resource group.<br />
Image Builder must be authorized by an account owner.<br />
<a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow">
<small>Learn more</small></a>
</Text>
<a href="https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=b94bb246-b02c-4985-9c22-d44e66f657f4
&scope=openid&response_type=code&response_mode=form_post
&redirect_uri=https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient" target="_blank" rel="noopener noreferrer">
Authorize Image Builder on Azure <ExternalLinkAltIcon />
</a>
</TextContent>
<Title headingLevel="h3">Destination</Title>
<Text>
Your image will be uploaded to the resource group in the subscription you specify.
</Text>
<Form>
<FormGroup isRequired label="Tenant ID" fieldId="azure-tenant-id"
helperTextInvalid={ (this.props.errors['azure-tenant-id'] && this.props.errors['azure-tenant-id'].value) || '' }
validated={ (this.props.errors['azure-tenant-id'] && 'error') || 'default' }>
<TextInput value={ this.props.uploadAzure.tenantId || '' }
type="text" aria-label="Azure tenant-id" id="azure-tenant-id"
data-testid="azure-tenant-id" isRequired
onChange={ value =>
this.props.setUploadAzure(Object.assign(this.props.uploadAzure, { tenantId: value })) } />
</FormGroup>
<FormGroup isRequired label="Subscription ID" fieldId="azure-subscription-id"
helperTextInvalid={ (this.props.errors['azure-subscription-id'] &&
this.props.errors['azure-subscription-id'].value) || '' }
validated={ (this.props.errors['azure-subscription-id'] && 'error') || 'default' }>
<TextInput value={ this.props.uploadAzure.subscriptionId || '' }
type="text" aria-label="Azure subscription-id" id="azure-subscription-id"
data-testid="azure-subscription-id" isRequired
onChange={ value =>
this.props.setUploadAzure(Object.assign(this.props.uploadAzure, { subscriptionId: value })) } />
</FormGroup>
<FormGroup isRequired label="Resource group" fieldId="azure-resource-group"
helperTextInvalid={ (this.props.errors['azure-resource-group'] &&
this.props.errors['azure-resource-group'].value) || '' }
validated={ (this.props.errors['azure-resource-group'] && 'error') || 'default' }>
<TextInput value={ this.props.uploadAzure.resourceGroup || '' }
type="text" aria-label="Azure resource group" id="azure-resource-group"
data-testid="azure-resource-group" isRequired
onChange={ value =>
this.props.setUploadAzure(Object.assign(this.props.uploadAzure, { resourceGroup: value })) } />
</FormGroup>
</Form>
</>
);
}
};
function mapStateToProps(state) {
return {
uploadAzure: state.pendingCompose.uploadAzure,
};
}
function mapDispatchToProps(dispatch) {
return {
setUploadAzure: u => dispatch(actions.setUploadAzure(u)),
};
}
WizardStepUploadAzure.propTypes = {
setUploadAzure: PropTypes.func,
uploadAzure: PropTypes.object,
errors: PropTypes.object,
};
export default connect(mapStateToProps, mapDispatchToProps)(WizardStepUploadAzure);

View file

@ -0,0 +1,11 @@
.textcontent-azure {
margin-bottom: var(--pf-global--spacer--lg);
h3, h4 {
margin-top: var(--pf-global--spacer--sm);
margin-bottom: var(--pf-global--spacer--xs);
}
p {
margin-bottom: 0;
}
}

View file

@ -0,0 +1,162 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { actions } from '../../store/actions';
import { Form, FormGroup, TextList, TextListItem, Popover, Radio, TextContent, Text, TextInput, Title } from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';
const accountTypePopover = (
<Popover
hasAutoWidth
maxWidth='35rem'
headerContent={ 'Valid account types' }
bodyContent={ <TextContent>
<Text>The following account types can have an image shared with them:</Text>
<TextList>
<TextListItem>
<strong>Google account:</strong> A Google account represents a developer, an administrator,
or any other person who interacts with Google Cloud. e.g., <em>`alice@gmail.com`</em>.
</TextListItem>
<TextListItem>
<strong>Service account:</strong> A service account is an account for an application instead
of an individual end user. e.g., <em>`myapp@appspot.gserviceaccount.com`</em>.
</TextListItem>
<TextListItem>
<strong>Google group:</strong> A Google group is a named collection of Google accounts and
and service accounts. e.g., <em>`admins@example.com`</em>.
</TextListItem>
<TextListItem>
<strong>Google workspace domain/Cloud identity domain:</strong> A Google workspace or cloud identity
domain represents a virtual group of all the Google accounts in an organization. These domains
represent your organization&apos;s internet domain name. e.g., <em>`mycompany.com`</em>.
</TextListItem>
</TextList>
</TextContent> }>
<button
type="button"
aria-label="Account info"
aria-describedby="google-account-type"
className="pf-c-form__group-label-help">
<HelpIcon />
</button>
</Popover>
);
class WizardStepUploadGoogle extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Form>
<Title headingLevel="h2" size="xl">Target Environment - Google Cloud Platform</Title>
<p>
Your image will be uploaded to an account on Google Cloud Platform. <br />
The image will be shared with the email you provide below. <br />
Within the next 14 days you will need to copy the shared image to your own account.
After 14 days it will be unavailable and will have to be regenerated.
</p>
<FormGroup isRequired label="Type" labelIcon={ accountTypePopover } fieldId="google-account-type">
<Radio
onChange={ () => this.props.setUploadGoogle({ accountType: 'googleAccount', shareWithAccounts: [{ user: null }]}) }
isChecked={ this.props.uploadGoogle.accountType === 'googleAccount' }
label="Google account"
id="radio-google-account"
value="googleAccount" />
<Radio
onChange={ () => this.props.setUploadGoogle({ accountType: 'serviceAccount', shareWithAccounts: [{ serviceAccount: null }]}) }
isChecked={ this.props.uploadGoogle.accountType === 'serviceAccount' }
label="Service account"
id="radio-service-account"
value="serviceAccount" />
<Radio
onChange={ () => this.props.setUploadGoogle({ accountType: 'googleGroup', shareWithAccounts: [{ group: null }]}) }
isChecked={ this.props.uploadGoogle.accountType === 'googleGroup' }
label="Google group"
id="radio-google-group"
value="googleGroup" />
<Radio
onChange={ () => this.props.setUploadGoogle({ accountType: 'domain', shareWithAccounts: [{ domain: null }]}) }
isChecked={ this.props.uploadGoogle.accountType === 'domain' }
label="Google Workspace Domain or Cloud Identity Domain"
id="radio-domain"
value="domain" />
</FormGroup>
{this.props.uploadGoogle.accountType === 'googleAccount' && (
<FormGroup isRequired label="Email address" fieldId="user">
<TextInput
value={ this.props.uploadGoogle.shareWithAccounts[0] ?
this.props.uploadGoogle.shareWithAccounts[0].user || '' :
'' }
type="text" aria-label="Google email address" id="input-google-user"
data-testid="input-google-user" isRequired
onChange={ value => this.props.setUploadGoogle(
{ accountType: 'googleAccount', shareWithAccounts: [{ user: value }]}
) } />
</FormGroup>
)}
{this.props.uploadGoogle.accountType === 'serviceAccount' && (
<FormGroup isRequired label="Email address" fieldId="service-account">
<TextInput
value={ this.props.uploadGoogle.shareWithAccounts[0] ?
this.props.uploadGoogle.shareWithAccounts[0].serviceAccount || '' :
'' }
type="text" aria-label="Google email address" id="input-google-service-account"
data-testid="input-google-service-account" isRequired
onChange={ value => this.props.setUploadGoogle(
{ accountType: 'serviceAccount', shareWithAccounts: [{ serviceAccount: value }]}
) } />
</FormGroup>
)}
{this.props.uploadGoogle.accountType === 'googleGroup' && (
<FormGroup isRequired label="Email address" fieldId="group">
<TextInput
value={ this.props.uploadGoogle.shareWithAccounts[0] ?
this.props.uploadGoogle.shareWithAccounts[0].group || '' :
'' }
type="text" aria-label="Google email address" id="input-google-group"
data-testid="input-google-group" isRequired
onChange={ value => this.props.setUploadGoogle(
{ accountType: 'googleGroup', shareWithAccounts: [{ group: value }]}
) } />
</FormGroup>
)}
{this.props.uploadGoogle.accountType === 'domain' && (
<FormGroup isRequired label="Domain" fieldId="domain">
<TextInput
value={ this.props.uploadGoogle.shareWithAccounts[0] ?
this.props.uploadGoogle.shareWithAccounts[0].domain || '' :
'' }
type="text" aria-label="Google domain" id="input-google-domain"
data-testid="input-google-domain" isRequired
onChange={ value => this.props.setUploadGoogle(
{ accountType: 'domain', shareWithAccounts: [{ domain: value }]}
) } />
</FormGroup>
)}
</Form>
);
}
};
function mapStateToProps(state) {
return {
uploadGoogle: state.pendingCompose.uploadGoogle,
};
}
function mapDispatchToProps(dispatch) {
return {
setUploadGoogle: u => dispatch(actions.setUploadGoogle(u)),
};
}
WizardStepUploadGoogle.propTypes = {
setUploadGoogle: PropTypes.func,
uploadGoogle: PropTypes.object,
errors: PropTypes.object,
};
export default connect(mapStateToProps, mapDispatchToProps)(WizardStepUploadGoogle);

View file

@ -0,0 +1,97 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Flex, Spinner } from '@patternfly/react-core';
import { CheckCircleIcon, PendingIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
import './ImageBuildStatus.scss';
const ImageBuildStatus = (props) => {
const messages = {
success: [
{
icon: <CheckCircleIcon className="success" />,
text: 'Ready'
}
],
failure: [
{
icon: <ExclamationCircleIcon className="error" />,
text: 'Image build failed'
}
],
pending: [
{
icon: <PendingIcon />,
text: 'Image build, Upload, Cloud registration pending'
}
],
// Keep "running" for backward compatibility
running: [
{
icon: <Spinner size="md" />,
text: 'Image build in progress'
},
{
icon: <PendingIcon />,
text: 'Upload, Cloud registration pending'
}
],
building: [
{
icon: <Spinner size="md" />,
text: 'Image build in progress'
},
{
icon: <PendingIcon />,
text: 'Upload, Cloud registration pending'
}
],
uploading: [
{
icon: <CheckCircleIcon />,
text: 'Image build finished'
},
{
icon: <Spinner size="md" />,
text: 'Image upload in progress'
},
{
icon: <PendingIcon />,
text: 'Cloud registration pending'
}
],
registering: [
{
icon: <CheckCircleIcon />,
text: 'Image build finished'
},
{
icon: <CheckCircleIcon />,
text: 'Image upload finished'
},
{
icon: <Spinner size="md" />,
text: 'Cloud registration in progress'
}
]
};
return (
<React.Fragment>
{messages[props.status] &&
messages[props.status].map((message, key) => (
<Flex key={ key } className="pf-u-align-items-baseline pf-m-nowrap">
<div>{message.icon}</div>
<small>{message.text}</small>
</Flex>
))
}
</React.Fragment>
);
};
ImageBuildStatus.propTypes = {
status: PropTypes.string,
};
export default ImageBuildStatus;

View file

@ -0,0 +1,6 @@
.error {
color: var(--pf-global--danger-color--100);
}
.success {
color: var(--pf-global--success-color--100);
}

View file

@ -0,0 +1,157 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions } from '../../store/actions';
import { Link } from 'react-router-dom';
import { Table, TableHeader, TableBody, classNames, Visibility } from '@patternfly/react-table';
import { TableToolbar } from '@redhat-cloud-services/frontend-components';
import { Button,
ToolbarGroup, ToolbarItem,
EmptyState, EmptyStateVariant, EmptyStateIcon, EmptyStateBody, EmptyStateSecondaryActions,
Title } from '@patternfly/react-core';
import { ExternalLinkAltIcon, PlusCircleIcon } from '@patternfly/react-icons';
import ImageBuildStatus from './ImageBuildStatus';
import Release from './Release';
import Upload from './Upload';
import api from '../../api.js';
class ImagesTable extends Component {
constructor(props) {
super(props);
this.state = {
columns: [
{
title: 'Image'
},
'Target',
'Release',
'Status',
{
title: '',
props: { className: 'pf-u-text-align-right' },
columnTransforms: [
classNames(
Visibility.hiddenOnXs,
Visibility.hiddenOnSm,
Visibility.hiddenOnMd,
Visibility.visibleOnLg
)
]
}
]
};
this.pollComposeStatuses = this.pollComposeStatuses.bind(this);
}
componentDidMount() {
this.interval = setInterval(() => this.pollComposeStatuses(), 8000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
pollComposeStatuses() {
let { composeUpdated, composes } = this.props;
Object.entries(composes.byId).map(([ id, compose ]) => {
/* Skip composes that have been complete */
if (compose.image_status.status === 'success' || compose.image_status.status === 'failure') {
return;
}
api.getComposeStatus(id).then(response => {
const newCompose = Object.assign({}, compose, { image_status: response.image_status });
composeUpdated(newCompose);
});
});
}
render() {
let { composes } = this.props;
const rows = Object.entries(composes.byId).map(([ id, compose ]) => {
return {
cells: [
id,
{ title: <Upload uploadType={ compose.image_requests[0].image_type } /> },
{ title: <Release release={ compose.distribution } /> },
{ title: <ImageBuildStatus status={ compose.image_status.status } /> },
''
]
};
});
return (
<React.Fragment>
{ composes.allIds.length === 0 && (
<EmptyState variant={ EmptyStateVariant.large } data-testid="empty-state">
<EmptyStateIcon icon={ PlusCircleIcon } />
<Title headingLevel="h4" size="lg">
Create an image
</Title>
<EmptyStateBody>
Create OS images for deployment in Amazon Web Services,
Microsoft Azure and Google Cloud Platform. Images can
include a custom package set and an activation key to
automate the registration process.
</EmptyStateBody>
<Link to="/imagewizard" className="pf-c-button pf-m-primary" data-testid="create-image-action">
Create image
</Link>
<EmptyStateSecondaryActions>
<Button
component="a"
target="_blank"
variant="link"
icon={ <ExternalLinkAltIcon /> }
iconPosition="right"
isInline
href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8-beta/">
Documentation
</Button>
</EmptyStateSecondaryActions>
</EmptyState>
) || (
<React.Fragment>
<TableToolbar>
<ToolbarGroup>
<ToolbarItem>
<Link to="/imagewizard" className="pf-c-button pf-m-primary" data-testid="create-image-action">
Create image
</Link>
</ToolbarItem>
</ToolbarGroup>
</TableToolbar>
<Table
aria-label="Images"
rows={ rows }
cells={ this.state.columns }
data-testid="images-table">
<TableHeader />
<TableBody />
</Table>
</React.Fragment>
)}
</React.Fragment>
);
}
}
function mapStateToProps(state) {
return {
composes: state.composes,
};
}
function mapDispatchToProps(dispatch) {
return {
composeUpdated: (compose) => dispatch(actions.composeUpdated(compose)),
};
}
ImagesTable.propTypes = {
composes: PropTypes.object,
composeUpdated: PropTypes.func,
};
export default connect(mapStateToProps, mapDispatchToProps)(ImagesTable);

View file

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Label } from '@patternfly/react-core';
const Release = (props) => {
const releaseOptions = {
'rhel-8': 'RHEL 8.3',
'centos-8': 'CentOS Stream 8'
};
const release = releaseOptions[props.release] ? releaseOptions[props.release] : props.release;
return <Label color='blue'>{release}</Label>;
};
Release.propTypes = {
release: PropTypes.string,
};
export default Release;

View file

@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
const Upload = (props) => {
const uploadOptions = {
aws: 'Amazon Web Services',
azure: 'Microsoft Azure',
gcp: 'Google Cloud Platform',
};
return <>{ uploadOptions[props.uploadType] ? uploadOptions[props.uploadType] : props.uploadType }</>;
};
Upload.propTypes = {
uploadType: PropTypes.string,
};
export default Upload;

View file

@ -0,0 +1,56 @@
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { PageHeader, PageHeaderTitle } from '@redhat-cloud-services/frontend-components';
import { Button, Popover, TextContent, Text } from '@patternfly/react-core';
import { ExternalLinkAltIcon, HelpIcon } from '@patternfly/react-icons';
import ImagesTable from '../ImagesTable/ImagesTable';
import './LandingPage.scss';
class LandingPage extends Component {
constructor(props) {
super(props);
}
render() {
return (
<React.Fragment>
<PageHeader>
<PageHeaderTitle className="title" title="Image Builder" />
<Popover
headerContent={ 'About Image Builder' }
bodyContent={ <TextContent>
<Text>
Image Builder is a service that allows you to create RHEL images
and push them to cloud environments.
</Text>
<Button
component="a"
target="_blank"
variant="link"
icon={ <ExternalLinkAltIcon /> }
iconPosition="right"
isInline
href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8-beta/">
Documentation
</Button>
</TextContent> }>
<button
type="button"
aria-label="About image builder"
className="pf-c-form__group-label-help">
<HelpIcon />
</button>
</Popover>
</PageHeader>
<section className="pf-l-page__main-section pf-c-page__main-section">
<ImagesTable />
</section>
</React.Fragment>
);
}
}
export default withRouter(LandingPage);

View file

@ -0,0 +1,11 @@
.title {
display: inline;
}
.pf-c-form__group-label-help {
color: var(--pf-global--icon--Color--light);
}
.pf-c-form__group-label-help:active {
color: var(--pf-global--icon--Color--dark);
}

View file

@ -0,0 +1,23 @@
import React from 'react';
import { InfoCircleIcon } from '@patternfly/react-icons';
import { Button, EmptyState, EmptyStateVariant, EmptyStateIcon, EmptyStateBody, Title } from '@patternfly/react-core';
const PermissionDenied = () => {
return (
<EmptyState variant={ EmptyStateVariant.large } data-testid="empty-state-denied">
<EmptyStateIcon icon={ InfoCircleIcon } />
<Title headingLevel="h4" size="lg">
Image Builder is not quite ready
</Title>
<EmptyStateBody>
Image Builder is in early development and not ready for use yet.
If you&apos;re interested in trying it out once it reaches beta,
fill out your contact information in the sign up form, and we&apos;ll be in touch once it&apos;s ready.
</EmptyStateBody>
<Button id="beta-signup-button" variant="primary">Sign up</Button>
</EmptyState>
);
};
export default PermissionDenied;