From 4fa71cede87d6b9cf4d1cada6217a0552751405a Mon Sep 17 00:00:00 2001 From: Jacob Kozol Date: Mon, 23 May 2022 11:38:16 +0200 Subject: [PATCH] update style across the project The eslint updates require style changes in all components. --- config/dev.webpack.config.js | 56 +- config/prod.webpack.config.js | 34 +- src/App.js | 40 +- src/AppEntry.js | 15 +- .../CreateImageWizard/CreateImageWizard.js | 863 ++--- .../CreateImageWizard/ImageCreator.js | 105 +- .../formComponents/ActivationKeys.js | 117 +- .../formComponents/AzureAuthButton.js | 40 +- .../formComponents/AzureAuthExpandable.js | 61 +- .../formComponents/CustomSubmitButtons.js | 89 +- .../formComponents/FileSystemConfigToggle.js | 72 +- .../formComponents/FileSystemConfiguration.js | 662 ++-- .../ImageOutputReleaseSelect.js | 86 +- .../formComponents/MountPoint.js | 143 +- .../formComponents/Packages.js | 786 +++-- .../formComponents/RadioWithPopover.js | 20 +- .../formComponents/ReviewStep.js | 696 ++-- .../formComponents/SizeUnit.js | 115 +- .../formComponents/TargetEnvironment.js | 255 +- src/Components/CreateImageWizard/steps/aws.js | 73 +- .../steps/fileSystemConfiguration.js | 75 +- .../CreateImageWizard/steps/googleCloud.js | 258 +- .../CreateImageWizard/steps/imageName.js | 50 +- .../CreateImageWizard/steps/imageOutput.js | 75 +- .../steps/imageOutputStepMapper.js | 25 +- .../CreateImageWizard/steps/msAzure.js | 198 +- .../CreateImageWizard/steps/packages.js | 41 +- .../CreateImageWizard/steps/registration.js | 239 +- .../CreateImageWizard/steps/review.js | 22 +- .../CreateImageWizard/steps/stepTemplate.js | 55 +- .../fileSystemConfigurationValidator.js | 42 +- .../validators/targetEnvironmentValidator.js | 23 +- .../ImagesTable/ImageBuildErrorDetails.js | 38 +- .../ImagesTable/ImageBuildStatus.js | 122 +- src/Components/ImagesTable/ImageLink.js | 211 +- src/Components/ImagesTable/ImagesTable.js | 422 ++- src/Components/ImagesTable/Release.js | 20 +- src/Components/ImagesTable/Target.js | 38 +- src/Components/LandingPage/LandingPage.js | 98 +- .../sharedComponents/DocumentationButton.js | 27 +- src/Router.js | 16 +- src/Utilities/getBaseName.js | 16 +- src/Utilities/isRhel.js | 19 +- src/api.js | 79 +- src/constants.js | 8 +- src/entry.test.js | 18 +- src/store/actions/actions.js | 207 +- src/store/index.js | 36 +- src/store/reducers/composes.js | 122 +- src/store/types.js | 26 +- .../CreateImageWizard.test.js | 3033 +++++++++-------- .../ImagesTable/ImagesTable.test.js | 881 ++--- .../LandingPage/LandingPage.test.js | 36 +- src/test/redux/actions.test.js | 46 +- src/test/redux/reducers.test.js | 174 +- src/test/testUtils.js | 26 +- 56 files changed, 5973 insertions(+), 5177 deletions(-) diff --git a/config/dev.webpack.config.js b/config/dev.webpack.config.js index 80bd6715..7bf6be8b 100644 --- a/config/dev.webpack.config.js +++ b/config/dev.webpack.config.js @@ -4,38 +4,46 @@ const { GitRevisionPlugin } = require('git-revision-webpack-plugin'); const config = require('@redhat-cloud-services/frontend-components-config'); const webpackProxy = { - useProxy: true, - proxyVerbose: true, - env: `${process.env.STAGE ? 'stage' : 'prod'}-${process.env.BETA ? 'beta' : 'stable'}`, - appUrl: process.env.BETA ? '/beta/insights/image-builder' : '/insights/image-builder', + useProxy: true, + proxyVerbose: true, + env: `${process.env.STAGE ? 'stage' : 'prod'}-${ + process.env.BETA ? 'beta' : 'stable' + }`, + appUrl: process.env.BETA + ? '/beta/insights/image-builder' + : '/insights/image-builder', }; const { config: webpackConfig, plugins } = config({ - rootFolder: resolve(__dirname, '../'), - debug: true, - modules: [ 'image_builder' ], - useFileHash: false, - sassPrefix: '.imageBuilder, .image_builder', - deployment: process.env.BETA ? 'beta/apps' : 'apps', - ...(process.env.PROXY ? webpackProxy : {}) + rootFolder: resolve(__dirname, '../'), + debug: true, + modules: ['image_builder'], + useFileHash: false, + sassPrefix: '.imageBuilder, .image_builder', + deployment: process.env.BETA ? 'beta/apps' : 'apps', + ...(process.env.PROXY ? webpackProxy : {}), }); plugins.push( - require('@redhat-cloud-services/frontend-components-config/federated-modules')({ - root: resolve(__dirname, '../'), - useFileHash: false, - moduleName: 'image_builder', - exposes: { - './RootApp': resolve(__dirname, '../src/AppEntry.js'), - }, - }) + require('@redhat-cloud-services/frontend-components-config/federated-modules')( + { + root: resolve(__dirname, '../'), + useFileHash: false, + moduleName: 'image_builder', + exposes: { + './RootApp': resolve(__dirname, '../src/AppEntry.js'), + }, + } + ) ); -plugins.push(new DefinePlugin({ - COMMITHASH: JSON.stringify((new GitRevisionPlugin()).commithash()), -})); +plugins.push( + new DefinePlugin({ + COMMITHASH: JSON.stringify(new GitRevisionPlugin().commithash()), + }) +); module.exports = { - ...webpackConfig, - plugins + ...webpackConfig, + plugins, }; diff --git a/config/prod.webpack.config.js b/config/prod.webpack.config.js index 270acbad..9e7ea752 100644 --- a/config/prod.webpack.config.js +++ b/config/prod.webpack.config.js @@ -3,26 +3,30 @@ const { resolve } = require('path'); const { GitRevisionPlugin } = require('git-revision-webpack-plugin'); const config = require('@redhat-cloud-services/frontend-components-config'); const { config: webpackConfig, plugins } = config({ - rootFolder: resolve(__dirname, '../'), - modules: [ 'image_builder' ], - sassPrefix: '.imageBuilder, .image_builder', + rootFolder: resolve(__dirname, '../'), + modules: ['image_builder'], + sassPrefix: '.imageBuilder, .image_builder', }); plugins.push( - require('@redhat-cloud-services/frontend-components-config/federated-modules')({ - root: resolve(__dirname, '../'), - moduleName: 'image_builder', - exposes: { - './RootApp': resolve(__dirname, '../src/AppEntry.js'), - }, - }) + require('@redhat-cloud-services/frontend-components-config/federated-modules')( + { + root: resolve(__dirname, '../'), + moduleName: 'image_builder', + exposes: { + './RootApp': resolve(__dirname, '../src/AppEntry.js'), + }, + } + ) ); -plugins.push(new DefinePlugin({ - COMMITHASH: JSON.stringify((new GitRevisionPlugin()).commithash()), -})); +plugins.push( + new DefinePlugin({ + COMMITHASH: JSON.stringify(new GitRevisionPlugin().commithash()), + }) +); module.exports = { - ...webpackConfig, - plugins + ...webpackConfig, + plugins, }; diff --git a/src/App.js b/src/App.js index 2593234d..8aafc851 100644 --- a/src/App.js +++ b/src/App.js @@ -8,28 +8,28 @@ import NotificationsPortal from '@redhat-cloud-services/frontend-components-noti import { notificationsReducer } from '@redhat-cloud-services/frontend-components-notifications/redux'; const App = (props) => { - const navigate = useNavigate(); + const navigate = useNavigate(); - useEffect(() => { - const registry = getRegistry(); - registry.register({ notifications: notificationsReducer }); - document.title = 'Image Builder | Red Hat Insights'; - insights.chrome.init(); - insights.chrome.identifyApp('image-builder'); - const unregister = insights.chrome.on('APP_NAVIGATION', (event) => - navigate(`/${event.navId}`) - ); - return () => { - unregister(); - }; - }, []); - - return ( - - - - + useEffect(() => { + const registry = getRegistry(); + registry.register({ notifications: notificationsReducer }); + document.title = 'Image Builder | Red Hat Insights'; + insights.chrome.init(); + insights.chrome.identifyApp('image-builder'); + const unregister = insights.chrome.on('APP_NAVIGATION', (event) => + navigate(`/${event.navId}`) ); + return () => { + unregister(); + }; + }, []); + + return ( + + + + + ); }; export default App; diff --git a/src/AppEntry.js b/src/AppEntry.js index 1f513fad..69a73c18 100644 --- a/src/AppEntry.js +++ b/src/AppEntry.js @@ -7,11 +7,16 @@ import { getBaseName } from '@redhat-cloud-services/frontend-components-utilitie import logger from 'redux-logger'; const ImageBuilder = () => ( - - - - - + + + + + ); export default ImageBuilder; diff --git a/src/Components/CreateImageWizard/CreateImageWizard.js b/src/Components/CreateImageWizard/CreateImageWizard.js index 7384cf81..a6d62d63 100644 --- a/src/Components/CreateImageWizard/CreateImageWizard.js +++ b/src/Components/CreateImageWizard/CreateImageWizard.js @@ -12,469 +12,506 @@ import { composeAdded } from '../../store/actions/actions'; import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux'; import { - review, - awsTarget, - registration, - googleCloudTarger, - msAzureTarget, - packages, - imageOutput, - fileSystemConfiguration, - imageName + review, + awsTarget, + registration, + googleCloudTarger, + msAzureTarget, + packages, + imageOutput, + fileSystemConfiguration, + imageName, } from './steps'; import { - fileSystemConfigurationValidator, - targetEnvironmentValidator, + fileSystemConfigurationValidator, + targetEnvironmentValidator, } from './validators'; const handleKeyDown = (e, handleClose) => { - if (e.key === 'Escape') { - handleClose(); - } + if (e.key === 'Escape') { + handleClose(); + } }; const onSave = (values) => { - let customizations = { - packages: values['selected-packages']?.map(p => p.name), + let customizations = { + packages: values['selected-packages']?.map((p) => p.name), + }; + + if (values['register-system'] === 'register-now-insights') { + customizations.subscription = { + 'activation-key': values['subscription-activation-key'], + insights: true, + organization: Number(values['subscription-organization-id']), + 'server-url': 'subscription.rhsm.redhat.com', + 'base-url': 'https://cdn.redhat.com/', + }; + } else if (values['register-system'] === 'register-now') { + customizations.subscription = { + 'activation-key': values['subscription-activation-key'], + insights: false, + organization: Number(values['subscription-organization-id']), + 'server-url': 'subscription.rhsm.redhat.com', + 'base-url': 'https://cdn.redhat.com/', + }; + } + + if (values['file-system-config-toggle'] === 'manual') { + customizations.filesystem = []; + for (let fsc of values['file-system-configuration']) { + customizations.filesystem.push({ + mountpoint: fsc.mountpoint, + min_size: fsc.size * fsc.unit, + }); + } + } + + let requests = []; + if (values['target-environment']?.aws) { + let request = { + distribution: values.release, + image_name: values?.['image-name'], + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: { + share_with_accounts: [values['aws-account-id']], + }, + }, + }, + ], + customizations, + }; + requests.push(request); + } + + if (values['target-environment']?.gcp) { + let share = ''; + switch (values['google-account-type']) { + case 'googleAccount': + share = `user:${values['google-email']}`; + break; + case 'serviceAccount': + share = `serviceAccount:${values['google-email']}`; + break; + case 'googleGroup': + share = `group:${values['google-email']}`; + break; + case 'domain': + share = `domain:${values['google-domain']}`; + break; + } + + let request = { + distribution: values.release, + image_name: values?.['image-name'], + image_requests: [ + { + architecture: 'x86_64', + image_type: 'gcp', + upload_request: { + type: 'gcp', + options: { + share_with_accounts: [share], + }, + }, + }, + ], + customizations, }; - if (values['register-system'] === 'register-now-insights') { - customizations.subscription = { - 'activation-key': values['subscription-activation-key'], - insights: true, - organization: Number(values['subscription-organization-id']), - 'server-url': 'subscription.rhsm.redhat.com', - 'base-url': 'https://cdn.redhat.com/', - }; - } else if (values['register-system'] === 'register-now') { - customizations.subscription = { - 'activation-key': values['subscription-activation-key'], - insights: false, - organization: Number(values['subscription-organization-id']), - 'server-url': 'subscription.rhsm.redhat.com', - 'base-url': 'https://cdn.redhat.com/', - }; - } + requests.push(request); + } - if (values['file-system-config-toggle'] === 'manual') { - customizations.filesystem = []; - for (let fsc of values['file-system-configuration']) { - customizations.filesystem.push({ - mountpoint: fsc.mountpoint, - min_size: fsc.size * fsc.unit, - }); - } - } + if (values['target-environment']?.azure) { + let request = { + distribution: values.release, + image_name: values?.['image-name'], + image_requests: [ + { + architecture: 'x86_64', + image_type: 'vhd', + upload_request: { + type: 'azure', + options: { + tenant_id: values['azure-tenant-id'], + subscription_id: values['azure-subscription-id'], + resource_group: values['azure-resource-group'], + }, + }, + }, + ], + customizations, + }; + requests.push(request); + } - let requests = []; - if (values['target-environment']?.aws) { - let request = { - distribution: values.release, - image_name: values?.['image-name'], - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: { - share_with_accounts: [ values['aws-account-id'] ], - }, - }, - }], - customizations, - }; - requests.push(request); - } + if (values['target-environment']?.vsphere) { + let request = { + distribution: values.release, + image_name: values?.['image-name'], + image_requests: [ + { + architecture: 'x86_64', + image_type: 'vsphere', + upload_request: { + type: 'aws.s3', + options: {}, + }, + }, + ], + customizations, + }; + requests.push(request); + } - if (values['target-environment']?.gcp) { - let share = ''; - switch (values['google-account-type']) { - case 'googleAccount': - share = `user:${values['google-email']}`; - break; - case 'serviceAccount': - share = `serviceAccount:${values['google-email']}`; - break; - case 'googleGroup': - share = `group:${values['google-email']}`; - break; - case 'domain': - share = `domain:${values['google-domain']}`; - break; - } + if (values['target-environment']?.['guest-image']) { + let request = { + distribution: values.release, + image_name: values?.['image-name'], + image_requests: [ + { + architecture: 'x86_64', + image_type: 'guest-image', + upload_request: { + type: 'aws.s3', + options: {}, + }, + }, + ], + customizations, + }; + requests.push(request); + } - let request = { - distribution: values.release, - image_name: values?.['image-name'], - image_requests: [ - { - architecture: 'x86_64', - image_type: 'gcp', - upload_request: { - type: 'gcp', - options: { - share_with_accounts: [ share ], - }, - }, - }], - customizations, - }; + if (values['target-environment']?.['image-installer']) { + let request = { + distribution: values.release, + image_name: values?.['image-name'], + image_requests: [ + { + architecture: 'x86_64', + image_type: 'image-installer', + upload_request: { + type: 'aws.s3', + options: {}, + }, + }, + ], + customizations, + }; + requests.push(request); + } - requests.push(request); - } - - if (values['target-environment']?.azure) { - let request = { - distribution: values.release, - image_name: values?.['image-name'], - image_requests: [ - { - architecture: 'x86_64', - image_type: 'vhd', - upload_request: { - type: 'azure', - options: { - tenant_id: values['azure-tenant-id'], - subscription_id: values['azure-subscription-id'], - resource_group: values['azure-resource-group'], - }, - }, - }], - customizations, - }; - requests.push(request); - } - - if (values['target-environment']?.vsphere) { - let request = { - distribution: values.release, - image_name: values?.['image-name'], - image_requests: [ - { - architecture: 'x86_64', - image_type: 'vsphere', - upload_request: { - type: 'aws.s3', - options: {} - } - }], - customizations, - }; - requests.push(request); - } - - if (values['target-environment']?.['guest-image']) { - let request = { - distribution: values.release, - image_name: values?.['image-name'], - image_requests: [ - { - architecture: 'x86_64', - image_type: 'guest-image', - upload_request: { - type: 'aws.s3', - options: {} - } - }], - customizations, - }; - requests.push(request); - } - - if (values['target-environment']?.['image-installer']) { - let request = { - distribution: values.release, - image_name: values?.['image-name'], - image_requests: [ - { - architecture: 'x86_64', - image_type: 'image-installer', - upload_request: { - type: 'aws.s3', - options: {} - } - }], - customizations, - }; - requests.push(request); - } - - return requests; + return requests; }; const parseSizeUnit = (bytesize) => { - let size; - let unit; + let size; + let unit; - if (bytesize % UNIT_GIB === 0) { - size = bytesize / UNIT_GIB; - unit = UNIT_GIB; - } else if (bytesize % UNIT_MIB === 0) { - size = bytesize / UNIT_MIB; - unit = UNIT_MIB; - } else if (bytesize % UNIT_KIB === 0) { - size = bytesize / UNIT_KIB; - unit = UNIT_KIB; - } + if (bytesize % UNIT_GIB === 0) { + size = bytesize / UNIT_GIB; + unit = UNIT_GIB; + } else if (bytesize % UNIT_MIB === 0) { + size = bytesize / UNIT_MIB; + unit = UNIT_MIB; + } else if (bytesize % UNIT_KIB === 0) { + size = bytesize / UNIT_KIB; + unit = UNIT_KIB; + } - return [ size, unit ]; + return [size, unit]; }; const getPackageDescription = async (release, arch, packageName) => { - const args = [ - release, - arch, - packageName - ]; - let { data, meta } = await api.getPackages(...args); - let summary; + const args = [release, arch, packageName]; + let { data, meta } = await api.getPackages(...args); + let summary; - // the package should be found in the 0 index - // if not then fetch all package matches and search for the package - if (data[0]?.name === packageName) { - summary = data[0]?.summary; - } else { - if (data?.length !== meta.count) { - ({ data } = await api.getPackages(...args, meta.count)); - } - - const pack = data.find(pack => packageName === pack.name); - summary = pack?.summary; + // the package should be found in the 0 index + // if not then fetch all package matches and search for the package + if (data[0]?.name === packageName) { + summary = data[0]?.summary; + } else { + if (data?.length !== meta.count) { + ({ data } = await api.getPackages(...args, meta.count)); } - // if no matching package is found return an empty string for description - return summary || ''; -} ; + const pack = data.find((pack) => packageName === pack.name); + summary = pack?.summary; + } + + // if no matching package is found return an empty string for description + return summary || ''; +}; // map the compose request object to the expected form state const requestToState = (composeRequest) => { - if (composeRequest) { - const imageRequest = composeRequest.image_requests[0]; - const uploadRequest = imageRequest.upload_request; - let formState = {}; + if (composeRequest) { + const imageRequest = composeRequest.image_requests[0]; + const uploadRequest = imageRequest.upload_request; + let formState = {}; - formState['image-name'] = composeRequest.image_name; + formState['image-name'] = composeRequest.image_name; - formState.release = composeRequest?.distribution; - // set defaults for target environment first - formState['target-environment'] = { - aws: false, - azure: false, - gcp: false, - 'guest-image': false, - }; - // then select the one from the request - // if the image type is to a cloud provider we use the upload_request.type - // or if the image is intended for download we use the image_type - let targetEnvironment; - if (uploadRequest.type === 'aws.s3') { - targetEnvironment = imageRequest.image_type; - } else { - targetEnvironment = uploadRequest.type; - } - - formState['target-environment'][targetEnvironment] = true; - - if (targetEnvironment === 'aws') { - formState['aws-account-id'] = uploadRequest?.options?.share_with_accounts[0]; - } else if (targetEnvironment === 'azure') { - formState['azure-tenant-id'] = uploadRequest?.options?.tenant_id; - formState['azure-subscription-id'] = uploadRequest?.options?.subscription_id; - formState['azure-resource-group'] = uploadRequest?.options?.resource_group; - } else if (targetEnvironment === 'gcp') { - // parse google account info - // roughly in the format `accountType:accountEmail` - const accountInfo = uploadRequest?.options?.share_with_accounts[0]; - const [ accountTypePrefix, account ] = accountInfo.split(':'); - - switch (accountTypePrefix) { - case 'user': - formState['google-account-type'] = 'googleAccount'; - formState['google-email'] = account; - break; - case 'serviceAccount': - formState['google-account-type'] = 'serviceAccount'; - formState['google-email'] = account; - break; - case 'group': - formState['google-account-type'] = 'googleGroup'; - formState['google-email'] = account; - break; - case 'domain': - formState['google-account-type'] = 'domain'; - formState['google-domain'] = account; - break; - } - } - - // customizations - // packages - let packs = []; - composeRequest?.customizations?.packages?.forEach(async (packName) => { - const packageDescription = await getPackageDescription(composeRequest?.distribution, imageRequest?.architecture, packName); - const pack = ({ - name: packName, - summary: packageDescription - }); - packs.push(pack); - }); - formState['selected-packages'] = packs; - - // filesystem - const fs = composeRequest?.customizations?.filesystem; - if (fs) { - formState['file-system-config-toggle'] = 'manual'; - let fileSystemConfiguration = []; - for (let fsc of fs) { - const [ size, unit ] = parseSizeUnit(fsc.min_size); - fileSystemConfiguration.push({ - mountpoint: fsc.mountpoint, - size, - unit - }); - } - - formState['file-system-configuration'] = fileSystemConfiguration; - } - - // subscription - const subscription = composeRequest?.customizations?.subscription; - if (subscription) { - if (subscription.insights) { - formState['register-system'] = 'register-now-insights'; - } else { - formState['register-system'] = 'register-now'; - } - - formState['subscription-activation-key'] = subscription['activation-key']; - formState['subscription-organization-id'] = subscription.organization; - } else { - formState['register-system'] = 'register-later'; - } - - return formState; + formState.release = composeRequest?.distribution; + // set defaults for target environment first + formState['target-environment'] = { + aws: false, + azure: false, + gcp: false, + 'guest-image': false, + }; + // then select the one from the request + // if the image type is to a cloud provider we use the upload_request.type + // or if the image is intended for download we use the image_type + let targetEnvironment; + if (uploadRequest.type === 'aws.s3') { + targetEnvironment = imageRequest.image_type; } else { - return; + targetEnvironment = uploadRequest.type; } + + formState['target-environment'][targetEnvironment] = true; + + if (targetEnvironment === 'aws') { + formState['aws-account-id'] = + uploadRequest?.options?.share_with_accounts[0]; + } else if (targetEnvironment === 'azure') { + formState['azure-tenant-id'] = uploadRequest?.options?.tenant_id; + formState['azure-subscription-id'] = + uploadRequest?.options?.subscription_id; + formState['azure-resource-group'] = + uploadRequest?.options?.resource_group; + } else if (targetEnvironment === 'gcp') { + // parse google account info + // roughly in the format `accountType:accountEmail` + const accountInfo = uploadRequest?.options?.share_with_accounts[0]; + const [accountTypePrefix, account] = accountInfo.split(':'); + + switch (accountTypePrefix) { + case 'user': + formState['google-account-type'] = 'googleAccount'; + formState['google-email'] = account; + break; + case 'serviceAccount': + formState['google-account-type'] = 'serviceAccount'; + formState['google-email'] = account; + break; + case 'group': + formState['google-account-type'] = 'googleGroup'; + formState['google-email'] = account; + break; + case 'domain': + formState['google-account-type'] = 'domain'; + formState['google-domain'] = account; + break; + } + } + + // customizations + // packages + let packs = []; + composeRequest?.customizations?.packages?.forEach(async (packName) => { + const packageDescription = await getPackageDescription( + composeRequest?.distribution, + imageRequest?.architecture, + packName + ); + const pack = { + name: packName, + summary: packageDescription, + }; + packs.push(pack); + }); + formState['selected-packages'] = packs; + + // filesystem + const fs = composeRequest?.customizations?.filesystem; + if (fs) { + formState['file-system-config-toggle'] = 'manual'; + let fileSystemConfiguration = []; + for (let fsc of fs) { + const [size, unit] = parseSizeUnit(fsc.min_size); + fileSystemConfiguration.push({ + mountpoint: fsc.mountpoint, + size, + unit, + }); + } + + formState['file-system-configuration'] = fileSystemConfiguration; + } + + // subscription + const subscription = composeRequest?.customizations?.subscription; + if (subscription) { + if (subscription.insights) { + formState['register-system'] = 'register-now-insights'; + } else { + formState['register-system'] = 'register-now'; + } + + formState['subscription-activation-key'] = subscription['activation-key']; + formState['subscription-organization-id'] = subscription.organization; + } else { + formState['register-system'] = 'register-later'; + } + + return formState; + } else { + return; + } }; const formStepHistory = (composeRequest) => { - if (composeRequest) { - const imageRequest = composeRequest.image_requests[0]; - const uploadRequest = imageRequest.upload_request; - let steps = [ - 'image-output' - ]; + if (composeRequest) { + const imageRequest = composeRequest.image_requests[0]; + const uploadRequest = imageRequest.upload_request; + let steps = ['image-output']; - if (uploadRequest.type === 'aws') { - steps.push('aws-target-env'); - } else if (uploadRequest.type === 'azure') { - steps.push('azure-target-env'); - } else if (uploadRequest.type === 'gcp') { - steps.push('google-cloud-target-env'); - } - - if (isRhel(composeRequest?.distribution)) { - steps.push('registration'); - } - - steps = steps.concat([ - 'File system configuration', - 'packages', - 'image-name' - ]); - - return steps; - } else { - return []; + if (uploadRequest.type === 'aws') { + steps.push('aws-target-env'); + } else if (uploadRequest.type === 'azure') { + steps.push('azure-target-env'); + } else if (uploadRequest.type === 'gcp') { + steps.push('google-cloud-target-env'); } + + if (isRhel(composeRequest?.distribution)) { + steps.push('registration'); + } + + steps = steps.concat([ + 'File system configuration', + 'packages', + 'image-name', + ]); + + return steps; + } else { + return []; + } }; const CreateImageWizard = () => { - const dispatch = useDispatch(); - const navigate = useNavigate(); - const location = useLocation(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const location = useLocation(); - const composeRequest = location?.state?.composeRequest; - const initialState = requestToState(composeRequest); - const stepHistory = formStepHistory(composeRequest); + const composeRequest = location?.state?.composeRequest; + const initialState = requestToState(composeRequest); + const stepHistory = formStepHistory(composeRequest); - const handleClose = () => navigate('/'); + const handleClose = () => navigate('/'); - return { - setIsSaving(() => true); - const requests = onSave(values); - Promise.all(requests.map(request => api.composeImage(request).then((response) => { - dispatch(composeAdded({ + return ( + { + setIsSaving(() => true); + const requests = onSave(values); + Promise.all( + requests.map((request) => + api.composeImage(request).then((response) => { + dispatch( + composeAdded( + { ...response, request, - image_status: { status: 'pending' } - }, true)); - }))) - .then(() => { - navigate('/'); - dispatch(addNotification({ - variant: 'success', - title: 'Your image is being created', - })); + image_status: { status: 'pending' }, + }, + true + ) + ); + }) + ) + ) + .then(() => { + navigate('/'); + dispatch( + addNotification({ + variant: 'success', + title: 'Your image is being created', + }) + ); - setIsSaving(false); - }) - .catch((err) => { - dispatch(addNotification({ - variant: 'danger', - title: 'Your image could not be created', - description: 'Status code ' + err.response.status + ': ' + err.response.statusText, - })); + setIsSaving(false); + }) + .catch((err) => { + dispatch( + addNotification({ + variant: 'danger', + title: 'Your image could not be created', + description: + 'Status code ' + + err.response.status + + ': ' + + err.response.statusText, + }) + ); - setIsSaving(false); - }); - } } - defaultArch="x86_64" - customValidatorMapper={ { fileSystemConfigurationValidator, targetEnvironmentValidator } } - schema={ { + setIsSaving(false); + }); + }} + defaultArch="x86_64" + customValidatorMapper={{ + fileSystemConfigurationValidator, + targetEnvironmentValidator, + }} + schema={{ + fields: [ + { + component: componentTypes.WIZARD, + name: 'image-builder-wizard', + className: 'image_builder', + isDynamic: true, + inModal: true, + onKeyDown: (e) => { + handleKeyDown(e, handleClose); + }, + buttonLabels: { + submit: 'Create image', + }, + showTitles: true, + title: 'Create image', + crossroads: ['target-environment', 'release'], + description: ( + <> + Image builder allows you to create a custom image and push it to + target environments. + + ), + // order in this array does not reflect order in wizard nav, this order is managed inside + // of each step by `nextStep` property! fields: [ - { - component: componentTypes.WIZARD, - name: 'image-builder-wizard', - className: 'image_builder', - isDynamic: true, - inModal: true, - onKeyDown: (e) => { handleKeyDown(e, handleClose); }, - buttonLabels: { - submit: 'Create image', - }, - showTitles: true, - title: 'Create image', - crossroads: [ 'target-environment', 'release' ], - description: <>Image builder allows you to create a custom image and push it to target environments. , - // order in this array does not reflect order in wizard nav, this order is managed inside - // of each step by `nextStep` property! - fields: [ - imageOutput, - awsTarget, - googleCloudTarger, - msAzureTarget, - registration, - packages, - fileSystemConfiguration, - imageName, - review, - ], - initialState: { - activeStep: location?.state?.initialStep || 'image-output', // name of the active step - activeStepIndex: stepHistory.length, // active index - maxStepIndex: stepHistory.length, // max achieved index - prevSteps: stepHistory, // array with names of previously visited steps - } - } - ] - } } - initialValues={ initialState } />; + imageOutput, + awsTarget, + googleCloudTarger, + msAzureTarget, + registration, + packages, + fileSystemConfiguration, + imageName, + review, + ], + initialState: { + activeStep: location?.state?.initialStep || 'image-output', // name of the active step + activeStepIndex: stepHistory.length, // active index + maxStepIndex: stepHistory.length, // max achieved index + prevSteps: stepHistory, // array with names of previously visited steps + }, + }, + ], + }} + initialValues={initialState} + /> + ); }; export default CreateImageWizard; diff --git a/src/Components/CreateImageWizard/ImageCreator.js b/src/Components/CreateImageWizard/ImageCreator.js index 062e61df..9728702d 100644 --- a/src/Components/CreateImageWizard/ImageCreator.js +++ b/src/Components/CreateImageWizard/ImageCreator.js @@ -16,52 +16,71 @@ import FileSystemConfiguration from './formComponents/FileSystemConfiguration'; import FileSystemConfigToggle from './formComponents/FileSystemConfigToggle'; import ImageOutputReleaseSelect from './formComponents/ImageOutputReleaseSelect'; -const ImageCreator = ({ schema, onSubmit, onClose, customComponentMapper, customValidatorMapper, defaultArch, className, ...props }) => { - return schema ? } - onSubmit={ (formValues) => onSubmit(formValues) } - validatorMapper={ { ...customValidatorMapper } } - componentMapper={ { - ...componentMapper, - review: Review, - output: TargetEnvironment, - select: Select, - 'package-selector': { - component: Packages, - defaultArch - }, - 'radio-popover': RadioWithPopover, - 'azure-auth-expandable': AzureAuthExpandable, - 'azure-auth-button': AzureAuthButton, - 'activation-keys': ActivationKeys, - 'file-system-config-toggle': FileSystemConfigToggle, - 'file-system-configuration': FileSystemConfiguration, - 'image-output-release-select': ImageOutputReleaseSelect, - ...customComponentMapper, - } } - onCancel={ onClose } - { ...props } /> : ; +const ImageCreator = ({ + schema, + onSubmit, + onClose, + customComponentMapper, + customValidatorMapper, + defaultArch, + className, + ...props +}) => { + return schema ? ( + ( + + )} + onSubmit={(formValues) => onSubmit(formValues)} + validatorMapper={{ ...customValidatorMapper }} + componentMapper={{ + ...componentMapper, + review: Review, + output: TargetEnvironment, + select: Select, + 'package-selector': { + component: Packages, + defaultArch, + }, + 'radio-popover': RadioWithPopover, + 'azure-auth-expandable': AzureAuthExpandable, + 'azure-auth-button': AzureAuthButton, + 'activation-keys': ActivationKeys, + 'file-system-config-toggle': FileSystemConfigToggle, + 'file-system-configuration': FileSystemConfiguration, + 'image-output-release-select': ImageOutputReleaseSelect, + ...customComponentMapper, + }} + onCancel={onClose} + {...props} + /> + ) : ( + + ); }; ImageCreator.propTypes = { - schema: PropTypes.object, - onSubmit: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - customComponentMapper: PropTypes.shape({ - [PropTypes.string]: PropTypes.oneOfType([ PropTypes.node, PropTypes.shape({ - component: PropTypes.node - }) ]) - }), - customValidatorMapper: PropTypes.shape({ - [PropTypes.string]: PropTypes.func - }), - defaultArch: PropTypes.string, - className: PropTypes.string, - initialValues: PropTypes.object + schema: PropTypes.object, + onSubmit: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + customComponentMapper: PropTypes.shape({ + [PropTypes.string]: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.shape({ + component: PropTypes.node, + }), + ]), + }), + customValidatorMapper: PropTypes.shape({ + [PropTypes.string]: PropTypes.func, + }), + defaultArch: PropTypes.string, + className: PropTypes.string, + initialValues: PropTypes.object, }; export default ImageCreator; diff --git a/src/Components/CreateImageWizard/formComponents/ActivationKeys.js b/src/Components/CreateImageWizard/formComponents/ActivationKeys.js index 9ca77a68..1c334db7 100644 --- a/src/Components/CreateImageWizard/formComponents/ActivationKeys.js +++ b/src/Components/CreateImageWizard/formComponents/ActivationKeys.js @@ -1,71 +1,86 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { FormGroup, Spinner, Select, SelectOption, SelectVariant } from '@patternfly/react-core'; +import { + FormGroup, + Spinner, + Select, + SelectOption, + SelectVariant, +} from '@patternfly/react-core'; import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api'; import api from '../../../api'; const ActivationKeys = ({ label, isRequired, ...props }) => { - const { change, getState } = useFormApi(); - const { input } = useFieldApi(props); - const [ activationKeys, setActivationKeys ] = useState([]); - const [ isOpen, setIsOpen ] = useState(false); - const [ isLoading, setIsLoading ] = useState(false); - const [ activationKeySelected, selectActivationKey ] = useState(getState()?.values?.['subscription-activation-key']); + const { change, getState } = useFormApi(); + const { input } = useFieldApi(props); + const [activationKeys, setActivationKeys] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [activationKeySelected, selectActivationKey] = useState( + getState()?.values?.['subscription-activation-key'] + ); - useEffect(() => { - setIsLoading(true); - const data = api.getActivationKeys(); - data.then(keys => { - setActivationKeys(keys); - setIsLoading(false); - }); - }, []); + useEffect(() => { + setIsLoading(true); + const data = api.getActivationKeys(); + data.then((keys) => { + setActivationKeys(keys); + setIsLoading(false); + }); + }, []); - const setActivationKey = (_, selection) => { - selectActivationKey(selection); - setIsOpen(false); - change(input.name, selection); - }; + const setActivationKey = (_, selection) => { + selectActivationKey(selection); + setIsOpen(false); + change(input.name, selection); + }; - const handleClear = () => { - selectActivationKey(); - change(input.name, undefined); - }; + const handleClear = () => { + selectActivationKey(); + change(input.name, undefined); + }; - return ( - - - ); + return ( + + + + ); }; ActivationKeys.propTypes = { - label: PropTypes.node, - isRequired: PropTypes.bool + label: PropTypes.node, + isRequired: PropTypes.bool, }; ActivationKeys.defaultProps = { - label: '', - isRequired: false + label: '', + isRequired: false, }; export default ActivationKeys; diff --git a/src/Components/CreateImageWizard/formComponents/AzureAuthButton.js b/src/Components/CreateImageWizard/formComponents/AzureAuthButton.js index 0936a66e..269aa3c8 100644 --- a/src/Components/CreateImageWizard/formComponents/AzureAuthButton.js +++ b/src/Components/CreateImageWizard/formComponents/AzureAuthButton.js @@ -3,24 +3,32 @@ import { Button, FormGroup } from '@patternfly/react-core'; import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; const AzureAuthButton = () => { - const { getState } = useFormApi(); + const { getState } = useFormApi(); - const tenantId = getState()?.values?.['azure-tenant-id']; - const guidRegex = new RegExp('^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', 'i'); + const tenantId = getState()?.values?.['azure-tenant-id']; + const guidRegex = new RegExp( + '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', + 'i' + ); - return ( - - - ); + return ( + + + + ); }; export default AzureAuthButton; diff --git a/src/Components/CreateImageWizard/formComponents/AzureAuthExpandable.js b/src/Components/CreateImageWizard/formComponents/AzureAuthExpandable.js index 029fb5a8..45312e13 100644 --- a/src/Components/CreateImageWizard/formComponents/AzureAuthExpandable.js +++ b/src/Components/CreateImageWizard/formComponents/AzureAuthExpandable.js @@ -3,34 +3,41 @@ import { Button, ExpandableSection, Text, Title } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; const AzureAuthExpandable = () => { - const [ expanded, setExpanded ] = useState(true); + const [expanded, setExpanded] = useState(true); - return ( - <> - Authorizing an Azure account } - onToggle={ () => setExpanded(!expanded) } - isExpanded={ expanded }> - - To authorize Image Builder to push images to Microsoft Azure, the account owner - must configure Image Builder as an authorized application for a specific tenant ID and give it the role of - "Contributor" to at least one resource group.
-
- - - -
- ); + return ( + <> + Authorizing an Azure account + } + onToggle={() => setExpanded(!expanded)} + isExpanded={expanded} + > + + To authorize Image Builder to push images to Microsoft Azure, the + account owner must configure Image Builder as an authorized + application for a specific tenant ID and give it the role of + "Contributor" to at least one resource group. +
+
+ + + +
+ + ); }; export default AzureAuthExpandable; diff --git a/src/Components/CreateImageWizard/formComponents/CustomSubmitButtons.js b/src/Components/CreateImageWizard/formComponents/CustomSubmitButtons.js index d839049f..73616ed0 100644 --- a/src/Components/CreateImageWizard/formComponents/CustomSubmitButtons.js +++ b/src/Components/CreateImageWizard/formComponents/CustomSubmitButtons.js @@ -4,45 +4,62 @@ import { FormSpy } from '@data-driven-forms/react-form-renderer'; import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context'; import PropTypes from 'prop-types'; -const CustomButtons = ({ buttonLabels: { cancel, submit, back }}) => { - const [ isSaving, setIsSaving ] = useState(false); - const { handlePrev, formOptions } = useContext(WizardContext); - return - {() => ( - - - -
- -
-
- )} -
; +const CustomButtons = ({ buttonLabels: { cancel, submit, back } }) => { + const [isSaving, setIsSaving] = useState(false); + const { handlePrev, formOptions } = useContext(WizardContext); + return ( + + {() => ( + + + +
+ +
+
+ )} +
+ ); }; CustomButtons.propTypes = { - buttonLabels: PropTypes.shape({ - cancel: PropTypes.node, - submit: PropTypes.node, - back: PropTypes.node, - }), - isSaving: PropTypes.bool + buttonLabels: PropTypes.shape({ + cancel: PropTypes.node, + submit: PropTypes.node, + back: PropTypes.node, + }), + isSaving: PropTypes.bool, }; export default CustomButtons; diff --git a/src/Components/CreateImageWizard/formComponents/FileSystemConfigToggle.js b/src/Components/CreateImageWizard/formComponents/FileSystemConfigToggle.js index c3ba512c..2ee904bf 100644 --- a/src/Components/CreateImageWizard/formComponents/FileSystemConfigToggle.js +++ b/src/Components/CreateImageWizard/formComponents/FileSystemConfigToggle.js @@ -1,48 +1,46 @@ -import React, { - useState, - useEffect, -} from 'react'; -import { - ToggleGroup, - ToggleGroupItem, -} from '@patternfly/react-core'; +import React, { useState, useEffect } from 'react'; +import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'; import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api'; const FileSystemConfigToggle = ({ ...props }) => { - const { change, getState } = useFormApi(); - const { input } = useFieldApi(props); - const [ selected, setSelected ] = - useState(getState()?.values?.['file-system-config-toggle'] || 'auto'); + const { change, getState } = useFormApi(); + const { input } = useFieldApi(props); + const [selected, setSelected] = useState( + getState()?.values?.['file-system-config-toggle'] || 'auto' + ); - useEffect(() => { - change(input.name, selected); - }, [ selected ]); + useEffect(() => { + change(input.name, selected); + }, [selected]); - const onClick = (_, evt) => { - setSelected(evt.currentTarget.id); - }; + const onClick = (_, evt) => { + setSelected(evt.currentTarget.id); + }; - return ( - <> - - - - - - ); + return ( + <> + + + + + + ); }; export default FileSystemConfigToggle; diff --git a/src/Components/CreateImageWizard/formComponents/FileSystemConfiguration.js b/src/Components/CreateImageWizard/formComponents/FileSystemConfiguration.js index 491e4460..0dc50cdb 100644 --- a/src/Components/CreateImageWizard/formComponents/FileSystemConfiguration.js +++ b/src/Components/CreateImageWizard/formComponents/FileSystemConfiguration.js @@ -1,24 +1,24 @@ -import React, { - useEffect, - useState, - useRef, -} from 'react'; -import { HelpIcon, MinusCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; +import React, { useEffect, useState, useRef } from 'react'; import { - Alert, - Button, - Popover, - Text, - TextContent, - TextVariants, + HelpIcon, + MinusCircleIcon, + PlusCircleIcon, +} from '@patternfly/react-icons'; +import { + Alert, + Button, + Popover, + Text, + TextContent, + TextVariants, } from '@patternfly/react-core'; import { - TableComposable, - Thead, - Tbody, - Tr, - Th, - Td, + TableComposable, + Thead, + Tbody, + Tr, + Th, + Td, } from '@patternfly/react-table'; import styles from '@patternfly/react-styles/css/components/Table/table'; import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; @@ -30,298 +30,352 @@ import MountPoint from './MountPoint'; import SizeUnit from './SizeUnit'; let initialRow = { - id: uuidv4(), - mountpoint: '/', - fstype: 'xfs', - size: 10, - unit: UNIT_GIB, + id: uuidv4(), + mountpoint: '/', + fstype: 'xfs', + size: 10, + unit: UNIT_GIB, }; const FileSystemConfiguration = ({ ...props }) => { - const { change, getState } = useFormApi(); - const { input } = useFieldApi(props); - const [ draggedItemId, setDraggedItemId ] = useState(null); - const [ draggingToItemIndex, setDraggingToItemIndex ] = useState(null); - const [ isDragging, setIsDragging ] = useState(false); - const [ itemOrder, setItemOrder ] = useState([ initialRow.id ]); - const [ tempItemOrder, setTempItemOrder ] = useState([]); - const bodyref = useRef(); - const [ rows, setRows ] = useState([ initialRow ]); + const { change, getState } = useFormApi(); + const { input } = useFieldApi(props); + const [draggedItemId, setDraggedItemId] = useState(null); + const [draggingToItemIndex, setDraggingToItemIndex] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [itemOrder, setItemOrder] = useState([initialRow.id]); + const [tempItemOrder, setTempItemOrder] = useState([]); + const bodyref = useRef(); + const [rows, setRows] = useState([initialRow]); - useEffect(() => { - const fsc = getState()?.values?.['file-system-configuration']; - if (!fsc) { - return; + useEffect(() => { + const fsc = getState()?.values?.['file-system-configuration']; + if (!fsc) { + return; + } + + const newRows = []; + const newOrder = []; + fsc.map((r) => { + const id = uuidv4(); + newRows.push({ + id, + mountpoint: r.mountpoint, + fstype: 'xfs', + size: r.size, + unit: r.unit, + }); + newOrder.push(id); + }); + setRows(newRows); + setItemOrder(newOrder); + }, []); + + useEffect(() => { + change( + input.name, + itemOrder.map((r) => { + for (const r2 of rows) { + if (r2.id === r) { + return { + mountpoint: r2.mountpoint, + size: r2.size, + unit: r2.unit, + }; + } } - - const newRows = []; - const newOrder = []; - fsc.map(r => { - const id = uuidv4(); - newRows.push({ - id, - mountpoint: r.mountpoint, - fstype: 'xfs', - size: r.size, - unit: r.unit, - }); - newOrder.push(id); - }); - setRows(newRows); - setItemOrder(newOrder); - }, []); - - useEffect(() => { - change(input.name, itemOrder.map(r => { - for (const r2 of rows) { - if (r2.id === r) { - return { - mountpoint: r2.mountpoint, - size: r2.size, - unit: r2.unit, - }; - } - } - })); - }, [ rows, itemOrder ]); - - const addRow = () => { - const id = uuidv4(); - setRows(rows.concat([{ - id, - mountpoint: '/home', - fstype: 'xfs', - size: 1, - unit: UNIT_GIB, - }])); - setItemOrder(itemOrder.concat([ id ])); - }; - - const removeRow = id => { - let removeIndex = rows.map(e => e.id).indexOf(id); - let newRows = [ ...rows ]; - newRows.splice(removeIndex, 1); - - let removeOrderIndex = itemOrder.indexOf(id); - let newOrder = [ ...itemOrder ]; - newOrder.splice(removeOrderIndex, 1); - - setRows(newRows); - setItemOrder(newOrder); - }; - - const moveItem = (arr, i1, toIndex) => { - const fromIndex = arr.indexOf(i1); - if (fromIndex === toIndex) { - return arr; - } - - const temp = arr.splice(fromIndex, 1); - arr.splice(toIndex, 0, temp[0]); - return arr; - }; - - const move = itemOrder => { - const ulNode = bodyref.current; - const nodes = Array.from(ulNode.children); - if (nodes.map(node => node.id).every((id, i) => id === itemOrder[i])) { - return; - } - - while (ulNode.firstChild) { - ulNode.removeChild(ulNode.lastChild); - } - - itemOrder.forEach(id => { - ulNode.appendChild(nodes.find(n => n.id === id)); - }); - }; - - const onDragOver = evt => { - evt.preventDefault(); - - const curListItem = evt.target.closest('tr'); - if (!curListItem || !bodyref.current.contains(curListItem)) { - return null; - } - - const dragId = curListItem.id; - const newDraggingToItemIndex = Array.from(bodyref.current.children).findIndex(item => item.id === dragId); - if (newDraggingToItemIndex !== draggingToItemIndex) { - const tempItemOrder = moveItem([ ...itemOrder ], draggedItemId, newDraggingToItemIndex); - move(tempItemOrder); - setDraggingToItemIndex(newDraggingToItemIndex); - setTempItemOrder(tempItemOrder); - } - }; - - const isValidDrop = evt => { - const ulRect = bodyref.current.getBoundingClientRect(); - return ( - evt.clientX > ulRect.x && - evt.clientX < ulRect.x + ulRect.width && - evt.clientY > ulRect.y && - evt.clientY < ulRect.y + ulRect.height - ); - }; - - const onDragLeave = evt => { - if (!isValidDrop(evt)) { - move(itemOrder); - setDraggingToItemIndex(null); - } - }; - - const onDrop = evt => { - if (isValidDrop(evt)) { - setItemOrder(tempItemOrder); - } - }; - - const onDragStart = evt => { - evt.dataTransfer.effectAllowed = 'move'; - evt.dataTransfer.setData('text/plain', evt.currentTarget.id); - evt.currentTarget.classList.add(styles.modifiers.ghostRow); - evt.currentTarget.setAttribute('aria-pressed', 'true'); - setDraggedItemId(evt.currentTarget.id); - setIsDragging(true); - }; - - const onDragEnd = evt => { - evt.target.classList.remove(styles.modifiers.ghostRow); - evt.target.setAttribute('aria-pressed', 'false'); - setDraggedItemId(null); - setDraggingToItemIndex(null); - setIsDragging(false); - }; - - const setMountpoint = (id, mp) => { - let newRows = [ ...rows ]; - for (let i = 0; i < newRows.length; i++) { - if (newRows[i].id === id) { - let newRow = { ...newRows[i] }; - newRow.mountpoint = mp; - newRows.splice(i, 1, newRow); - break; - } - } - - setRows(newRows); - }; - - const setSize = (id, s, u) => { - let newRows = [ ...rows ]; - for (let i = 0; i < newRows.length; i++) { - if (newRows[i].id === id) { - let newRow = { ...newRows[i] }; - newRow.size = s; - newRow.unit = u; - newRows.splice(i, 1, newRow); - break; - } - } - - setRows(newRows); - }; - - return ( - <> - - Configure partitions - - { rows.length > 1 && getState()?.errors?.['file-system-configuration']?.duplicates && - - } - { rows.length >= 1 && getState()?.errors?.['file-system-configuration']?.root === false && - - } - - - Partitions have been generated and given default values based on best practices from Red Hat, - and your selections in previous steps of the wizard. - - - - - - - Mount point - Type - Minimum size - - - Image Builder may extend this size based on requirements, selected packages, and configurations. - - }> - - - - - - - - {rows.map((row, rowIndex) => ( - - - - setMountpoint(row.id, mp) } /> - { getState().errors['file-system-configuration']?.duplicates && - getState().errors['file-system-configuration']?.duplicates.indexOf(row.mountpoint) !== -1 && - } - - - { /* always xfs */ } - {row.fstype} - - - setSize(row.id, s, u) } /> - - - - - + }) ); + }, [rows, itemOrder]); + + const addRow = () => { + const id = uuidv4(); + setRows( + rows.concat([ + { + id, + mountpoint: '/home', + fstype: 'xfs', + size: 1, + unit: UNIT_GIB, + }, + ]) + ); + setItemOrder(itemOrder.concat([id])); + }; + + const removeRow = (id) => { + let removeIndex = rows.map((e) => e.id).indexOf(id); + let newRows = [...rows]; + newRows.splice(removeIndex, 1); + + let removeOrderIndex = itemOrder.indexOf(id); + let newOrder = [...itemOrder]; + newOrder.splice(removeOrderIndex, 1); + + setRows(newRows); + setItemOrder(newOrder); + }; + + const moveItem = (arr, i1, toIndex) => { + const fromIndex = arr.indexOf(i1); + if (fromIndex === toIndex) { + return arr; + } + + const temp = arr.splice(fromIndex, 1); + arr.splice(toIndex, 0, temp[0]); + return arr; + }; + + const move = (itemOrder) => { + const ulNode = bodyref.current; + const nodes = Array.from(ulNode.children); + if (nodes.map((node) => node.id).every((id, i) => id === itemOrder[i])) { + return; + } + + while (ulNode.firstChild) { + ulNode.removeChild(ulNode.lastChild); + } + + itemOrder.forEach((id) => { + ulNode.appendChild(nodes.find((n) => n.id === id)); + }); + }; + + const onDragOver = (evt) => { + evt.preventDefault(); + + const curListItem = evt.target.closest('tr'); + if (!curListItem || !bodyref.current.contains(curListItem)) { + return null; + } + + const dragId = curListItem.id; + const newDraggingToItemIndex = Array.from( + bodyref.current.children + ).findIndex((item) => item.id === dragId); + if (newDraggingToItemIndex !== draggingToItemIndex) { + const tempItemOrder = moveItem( + [...itemOrder], + draggedItemId, + newDraggingToItemIndex + ); + move(tempItemOrder); + setDraggingToItemIndex(newDraggingToItemIndex); + setTempItemOrder(tempItemOrder); + } + }; + + const isValidDrop = (evt) => { + const ulRect = bodyref.current.getBoundingClientRect(); + return ( + evt.clientX > ulRect.x && + evt.clientX < ulRect.x + ulRect.width && + evt.clientY > ulRect.y && + evt.clientY < ulRect.y + ulRect.height + ); + }; + + const onDragLeave = (evt) => { + if (!isValidDrop(evt)) { + move(itemOrder); + setDraggingToItemIndex(null); + } + }; + + const onDrop = (evt) => { + if (isValidDrop(evt)) { + setItemOrder(tempItemOrder); + } + }; + + const onDragStart = (evt) => { + evt.dataTransfer.effectAllowed = 'move'; + evt.dataTransfer.setData('text/plain', evt.currentTarget.id); + evt.currentTarget.classList.add(styles.modifiers.ghostRow); + evt.currentTarget.setAttribute('aria-pressed', 'true'); + setDraggedItemId(evt.currentTarget.id); + setIsDragging(true); + }; + + const onDragEnd = (evt) => { + evt.target.classList.remove(styles.modifiers.ghostRow); + evt.target.setAttribute('aria-pressed', 'false'); + setDraggedItemId(null); + setDraggingToItemIndex(null); + setIsDragging(false); + }; + + const setMountpoint = (id, mp) => { + let newRows = [...rows]; + for (let i = 0; i < newRows.length; i++) { + if (newRows[i].id === id) { + let newRow = { ...newRows[i] }; + newRow.mountpoint = mp; + newRows.splice(i, 1, newRow); + break; + } + } + + setRows(newRows); + }; + + const setSize = (id, s, u) => { + let newRows = [...rows]; + for (let i = 0; i < newRows.length; i++) { + if (newRows[i].id === id) { + let newRow = { ...newRows[i] }; + newRow.size = s; + newRow.unit = u; + newRows.splice(i, 1, newRow); + break; + } + } + + setRows(newRows); + }; + + return ( + <> + + Configure partitions + + {rows.length > 1 && + getState()?.errors?.['file-system-configuration']?.duplicates && ( + + )} + {rows.length >= 1 && + getState()?.errors?.['file-system-configuration']?.root === false && ( + + )} + + + Partitions have been generated and given default values based on best + practices from Red Hat, and your selections in previous steps of the + wizard. + + + + + + + Mount point + Type + + Minimum size + + + Image Builder may extend this size based on requirements, + selected packages, and configurations. + + + } + > + + + + + + + + {rows.map((row, rowIndex) => ( + + + + setMountpoint(row.id, mp)} + /> + {getState().errors['file-system-configuration']?.duplicates && + getState().errors[ + 'file-system-configuration' + ]?.duplicates.indexOf(row.mountpoint) !== -1 && ( + + )} + + + {/* always xfs */} + {row.fstype} + + + setSize(row.id, s, u)} + /> + + + + + + ); }; export default FileSystemConfiguration; diff --git a/src/Components/CreateImageWizard/formComponents/ImageOutputReleaseSelect.js b/src/Components/CreateImageWizard/formComponents/ImageOutputReleaseSelect.js index e1371e57..1fc0bf9a 100644 --- a/src/Components/CreateImageWizard/formComponents/ImageOutputReleaseSelect.js +++ b/src/Components/CreateImageWizard/formComponents/ImageOutputReleaseSelect.js @@ -1,58 +1,64 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { FormGroup, Select, SelectOption, SelectVariant } from '@patternfly/react-core'; +import { + FormGroup, + Select, + SelectOption, + SelectVariant, +} from '@patternfly/react-core'; import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api'; import { RELEASES } from '../../../constants'; import isRhel from '../../../Utilities/isRhel'; const ImageOutputReleaseSelect = ({ label, isRequired, ...props }) => { - const { change, getState } = useFormApi(); - const { input } = useFieldApi(props); - const [ isOpen, setIsOpen ] = useState(false); + const { change, getState } = useFormApi(); + const { input } = useFieldApi(props); + const [isOpen, setIsOpen] = useState(false); - const setRelease = (_, selection) => { - change(input.name, selection); - setIsOpen(false); - }; + const setRelease = (_, selection) => { + change(input.name, selection); + setIsOpen(false); + }; - const handleClear = () => { - change(input.name, null); - }; + const handleClear = () => { + change(input.name, null); + }; - return ( - - setIsOpen(!isOpen)} + onSelect={setRelease} + onClear={handleClear} + selections={RELEASES[getState()?.values?.[input.name]]} + isOpen={isOpen} + > + {Object.entries(RELEASES) + .filter(([key]) => { + // Only show non-RHEL distros in beta + if (insights.chrome.isBeta()) { + return true; + } - return isRhel(key); - }) - .map(([ key, release ], index) => { - return - { release } - ; - }) - } - - - ); + return isRhel(key); + }) + .map(([key, release], index) => { + return ( + + {release} + + ); + })} + + + ); }; ImageOutputReleaseSelect.propTypes = { - label: PropTypes.node, - isRequired: PropTypes.bool + label: PropTypes.node, + isRequired: PropTypes.bool, }; export default ImageOutputReleaseSelect; diff --git a/src/Components/CreateImageWizard/formComponents/MountPoint.js b/src/Components/CreateImageWizard/formComponents/MountPoint.js index e969498e..33d44539 100644 --- a/src/Components/CreateImageWizard/formComponents/MountPoint.js +++ b/src/Components/CreateImageWizard/formComponents/MountPoint.js @@ -1,86 +1,95 @@ -import React, { - useEffect, - useState, -} from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { - Select, - SelectOption, - SelectVariant, - TextInput, + Select, + SelectOption, + SelectVariant, + TextInput, } from '@patternfly/react-core'; import path from 'path'; const MountPoint = ({ ...props }) => { - // check '/' last! - const validPrefixes = [ '/app', '/data', '/home', '/opt', '/srv', '/tmp', '/usr', '/usr/local', '/var', '/' ]; - const [ isOpen, setIsOpen ] = useState(false); - const [ prefix, setPrefix ] = useState('/'); - const [ suffix, setSuffix ] = useState(''); + // check '/' last! + const validPrefixes = [ + '/app', + '/data', + '/home', + '/opt', + '/srv', + '/tmp', + '/usr', + '/usr/local', + '/var', + '/', + ]; + const [isOpen, setIsOpen] = useState(false); + const [prefix, setPrefix] = useState('/'); + const [suffix, setSuffix] = useState(''); - // split - useEffect(() => { - for (let p of validPrefixes) { - if (props.mountpoint.startsWith(p)) { - setPrefix(p); - setSuffix(props.mountpoint.substring(p.length)); - return; - } - } - }, []); + // split + useEffect(() => { + for (let p of validPrefixes) { + if (props.mountpoint.startsWith(p)) { + setPrefix(p); + setSuffix(props.mountpoint.substring(p.length)); + return; + } + } + }, []); - useEffect(() => { - let suf = suffix; - let mp = prefix; - if (suf) { - if (mp !== '/' && suf[0] !== '/') { - suf = '/' + suf; - } + useEffect(() => { + let suf = suffix; + let mp = prefix; + if (suf) { + if (mp !== '/' && suf[0] !== '/') { + suf = '/' + suf; + } - mp += suf; - } + mp += suf; + } - props.onChange(path.normalize(mp)); - }, [ prefix, suffix ]); + props.onChange(path.normalize(mp)); + }, [prefix, suffix]); - const onToggle = (isOpen) => { - setIsOpen(isOpen); - }; + const onToggle = (isOpen) => { + setIsOpen(isOpen); + }; - const onSelect = (event, selection) => { - setPrefix(selection); - setIsOpen(false); - }; + const onSelect = (event, selection) => { + setPrefix(selection); + setIsOpen(false); + }; - return ( - <> - - { prefix !== '/' && - setSuffix(v) } /> - } - - ); + return ( + <> + + {prefix !== '/' && ( + setSuffix(v)} + /> + )} + + ); }; MountPoint.propTypes = { - mountpoint: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, + mountpoint: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, }; export default MountPoint; diff --git a/src/Components/CreateImageWizard/formComponents/Packages.js b/src/Components/CreateImageWizard/formComponents/Packages.js index 2b297561..2a652e7f 100644 --- a/src/Components/CreateImageWizard/formComponents/Packages.js +++ b/src/Components/CreateImageWizard/formComponents/Packages.js @@ -4,391 +4,459 @@ import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api'; import api from '../../../api'; import PropTypes from 'prop-types'; import { - DualListSelector, - DualListSelectorPane, - DualListSelectorList, - DualListSelectorListItem, - DualListSelectorControlsWrapper, - DualListSelectorControl, - SearchInput, - TextContent + DualListSelector, + DualListSelectorPane, + DualListSelectorList, + DualListSelectorListItem, + DualListSelectorControlsWrapper, + DualListSelectorControl, + SearchInput, + TextContent, } from '@patternfly/react-core'; -import { AngleDoubleLeftIcon, AngleLeftIcon, AngleDoubleRightIcon, AngleRightIcon } from '@patternfly/react-icons'; +import { + AngleDoubleLeftIcon, + AngleLeftIcon, + AngleDoubleRightIcon, + AngleRightIcon, +} from '@patternfly/react-icons'; // the fields isHidden and isSelected should not be included in the package list sent for image creation -const removePackagesDisplayFields = (packages) => packages.map((pack) => ({ +const removePackagesDisplayFields = (packages) => + packages.map((pack) => ({ name: pack.name, summary: pack.summary, -})); + })); const Packages = ({ defaultArch, ...props }) => { - const { change, getState } = useFormApi(); - const { input } = useFieldApi(props); - const [ packagesSearchName, setPackagesSearchName ] = useState(undefined); - const [ filterAvailable, setFilterAvailable ] = useState(undefined); - const [ filterChosen, setFilterChosen ] = useState(undefined); - const [ packagesAvailable, setPackagesAvailable ] = useState([]); - const [ packagesAvailableFound, setPackagesAvailableFound ] = useState(true); - const [ packagesChosen, setPackagesChosen ] = useState([]); - const [ packagesChosenFound, setPackagesChosenFound ] = useState(true); - const [ focus, setFocus ] = useState(''); + const { change, getState } = useFormApi(); + const { input } = useFieldApi(props); + const [packagesSearchName, setPackagesSearchName] = useState(undefined); + const [filterAvailable, setFilterAvailable] = useState(undefined); + const [filterChosen, setFilterChosen] = useState(undefined); + const [packagesAvailable, setPackagesAvailable] = useState([]); + const [packagesAvailableFound, setPackagesAvailableFound] = useState(true); + const [packagesChosen, setPackagesChosen] = useState([]); + const [packagesChosenFound, setPackagesChosenFound] = useState(true); + const [focus, setFocus] = useState(''); - // this effect only triggers on mount - useEffect(() => { - const selectedPackages = getState()?.values?.['selected-packages']; - if (selectedPackages) { - setPackagesChosen(selectedPackages); - } - }, []); + // this effect only triggers on mount + useEffect(() => { + const selectedPackages = getState()?.values?.['selected-packages']; + if (selectedPackages) { + setPackagesChosen(selectedPackages); + } + }, []); - const searchResultsComparator = useCallback((searchTerm) => { - return (a, b) => { - a = a.name.toLowerCase(); - b = b.name.toLowerCase(); + const searchResultsComparator = useCallback((searchTerm) => { + return (a, b) => { + a = a.name.toLowerCase(); + b = b.name.toLowerCase(); - // check exact match first - if (a === searchTerm) { - return -1; - } + // check exact match first + if (a === searchTerm) { + return -1; + } - if (b === searchTerm) { - return 1; - } + if (b === searchTerm) { + return 1; + } - // check for packages that start with the search term - if (a.startsWith(searchTerm) && !b.startsWith(searchTerm)) { - return -1; - } + // check for packages that start with the search term + if (a.startsWith(searchTerm) && !b.startsWith(searchTerm)) { + return -1; + } - if (b.startsWith(searchTerm) && !a.startsWith(searchTerm)) { - return 1; - } + if (b.startsWith(searchTerm) && !a.startsWith(searchTerm)) { + return 1; + } - // if both (or neither) start with the search term - // sort alphabetically - if (a < b) { - return -1; - } + // if both (or neither) start with the search term + // sort alphabetically + if (a < b) { + return -1; + } - if (b < a) { - return 1; - } + if (b < a) { + return 1; + } - return 0; - }; + return 0; + }; + }); + + const setPackagesAvailableSorted = ( + packageList, + filter = filterAvailable + ) => { + const sortResults = packageList.sort(searchResultsComparator(filter)); + setPackagesAvailable(sortResults); + }; + + const setPackagesChosenSorted = (packageList) => { + const sortResults = packageList.sort(searchResultsComparator(filterChosen)); + setPackagesChosen(sortResults); + }; + + // filter the packages by name + const filterPackagesAvailable = (packageList) => { + return packageList.filter((availablePackage) => { + // returns true if no packages in the available or chosen list have the same name + return !packagesChosen.some( + (chosenPackage) => availablePackage.name === chosenPackage.name + ); + }); + }; + + const getAllPackages = async () => { + const args = [ + getState()?.values?.release, + getState()?.values?.architecture || defaultArch, + packagesSearchName, + ]; + let { data, meta } = await api.getPackages(...args); + if (data?.length === meta.count) { + return data; + } else if (data) { + ({ data } = await api.getPackages(...args, meta.count)); + return data; + } + }; + + // call api to list available packages + const handlePackagesAvailableSearch = async () => { + setFilterAvailable(packagesSearchName); + + const packageList = await getAllPackages(); + if (packageList) { + const packagesAvailableFiltered = filterPackagesAvailable(packageList); + setPackagesAvailableSorted(packagesAvailableFiltered, packagesSearchName); + setPackagesAvailableFound( + packagesAvailableFiltered.length ? true : false + ); + } else { + setPackagesAvailable([]); + setPackagesAvailableFound(false); + } + }; + + // filter displayed selected packages + const handlePackagesChosenSearch = (val) => { + let found = false; + const filteredPackagesChosen = packagesChosen.map((pack) => { + if (!pack.name.includes(val)) { + pack.isHidden = true; + } else { + pack.isHidden = false; + found = true; + } + + return pack; }); - const setPackagesAvailableSorted = (packageList, filter = filterAvailable) => { - const sortResults = packageList.sort(searchResultsComparator(filter)); - setPackagesAvailable(sortResults); + setFilterChosen(val); + setPackagesChosenFound(found); + setPackagesChosenSorted(filteredPackagesChosen); + }; + + const keydownHandler = (event) => { + if (event.key === 'Enter') { + if (focus === 'available') { + event.stopPropagation(); + handlePackagesAvailableSearch(); + } + } + }; + + useEffect(() => { + document.addEventListener('keydown', keydownHandler, true); + + return () => { + document.removeEventListener('keydown', keydownHandler, true); }; + }); - const setPackagesChosenSorted = (packageList) => { - const sortResults = packageList.sort(searchResultsComparator(filterChosen)); - setPackagesChosen(sortResults); - }; + const areFound = (filter, packageList) => { + if (filter === undefined) { + return true; + } else if (packageList.some((pack) => pack.name.includes(filter))) { + return true; + } else { + return false; + } + }; - // filter the packages by name - const filterPackagesAvailable = (packageList) => { - return packageList.filter((availablePackage) => { - // returns true if no packages in the available or chosen list have the same name - return !packagesChosen.some((chosenPackage) => availablePackage.name === chosenPackage.name); - }); - }; + const isHidden = (filter, pack) => + filter && !pack.name.includes(filter) ? true : false; - const getAllPackages = async () => { - const args = [ - getState()?.values?.release, - getState()?.values?.architecture || defaultArch, - packagesSearchName - ]; - let { data, meta } = await api.getPackages(...args); - if (data?.length === meta.count) { - return data; - } else if (data) { - ({ data } = await api.getPackages(...args, meta.count)); - return data; - } - }; - - // call api to list available packages - const handlePackagesAvailableSearch = async () => { - setFilterAvailable(packagesSearchName); - - const packageList = await getAllPackages(); - if (packageList) { - const packagesAvailableFiltered = filterPackagesAvailable(packageList); - setPackagesAvailableSorted(packagesAvailableFiltered, packagesSearchName); - setPackagesAvailableFound(packagesAvailableFiltered.length ? true : false); - } else { - setPackagesAvailable([]); - setPackagesAvailableFound(false); - } - }; - - // filter displayed selected packages - const handlePackagesChosenSearch = (val) => { - let found = false; - const filteredPackagesChosen = packagesChosen.map((pack) => { - if (!pack.name.includes(val)) { - pack.isHidden = true; - } else { - pack.isHidden = false; - found = true; - } - - return pack; - }); - - setFilterChosen(val); - setPackagesChosenFound(found); - setPackagesChosenSorted(filteredPackagesChosen); - }; - - const keydownHandler = (event) => { - if (event.key === 'Enter') { - if (focus === 'available') { - event.stopPropagation(); - handlePackagesAvailableSearch(); - } - } - }; - - useEffect(() => { - document.addEventListener('keydown', keydownHandler, true); - - return () => { - document.removeEventListener('keydown', keydownHandler, true); - }; - }); - - const areFound = (filter, packageList) => { - if (filter === undefined) { - return true; - } else if (packageList.some(pack => pack.name.includes(filter))) { - return true; - } else { - return false; - } - }; - - const isHidden = (filter, pack) => filter && !pack.name.includes(filter) ? true : false; - - const updateState = (updatedPackagesAvailable, updatedPackagesChosen) => { - setPackagesChosenSorted(updatedPackagesChosen); - setPackagesAvailableSorted(updatedPackagesAvailable); - setPackagesAvailableFound(areFound(filterAvailable, updatedPackagesAvailable)); - setPackagesChosenFound(areFound(filterChosen, updatedPackagesChosen)); - // set the steps field to the current chosen packages list - change(input.name, removePackagesDisplayFields(updatedPackagesChosen)); - }; - - const moveSelectedToChosen = () => { - const newPackagesChosen = []; - - const updatedPackagesAvailable = packagesAvailable.filter((pack) => { - if (pack.selected) { - pack.selected = false; - pack.isHidden = isHidden(filterChosen, pack); - newPackagesChosen.push(pack); - return false; - } - - return true; - }); - - const updatedPackagesChosen = [ ...newPackagesChosen, ...packagesChosen ]; - - updateState(updatedPackagesAvailable, updatedPackagesChosen); - }; - - const moveSelectedToAvailable = () => { - const newPackagesAvailable = []; - - const updatedPackagesChosen = packagesChosen.filter((pack) => { - if (pack.selected) { - pack.selected = false; - pack.isHidden = false; - pack.name.includes(filterAvailable) ? newPackagesAvailable.push(pack) : null; - return false; - } - - return true; - }); - - const updatedPackagesAvailable = [ ...newPackagesAvailable, ...packagesAvailable ]; - - updateState(updatedPackagesAvailable, updatedPackagesChosen); - }; - - const moveAllToChosen = () => { - const newPackagesChosen = packagesAvailable.map(pack => { - return { ...pack, selected: false, isHidden: isHidden(filterChosen, pack) }; - }); - - const updatedPackagesAvailable = []; - const updatedPackagesChosen = [ ...newPackagesChosen, ...packagesChosen ]; - - updateState(updatedPackagesAvailable, updatedPackagesChosen); - }; - - const moveAllToAvailable = () => { - const updatedPackagesChosen = packagesChosen.filter(pack => pack.isHidden); - - const newPackagesAvailable = filterAvailable === undefined ? [] : - packagesChosen - .filter(pack => !pack.isHidden && pack.name.includes(filterAvailable)) - .map(pack => { return { ...pack, selected: false };}); - - const updatedPackagesAvailable = [ ...newPackagesAvailable, ...packagesAvailable ]; - - updateState(updatedPackagesAvailable, updatedPackagesChosen); - }; - - const onOptionSelect = (event, index, isChosen) => { - if (isChosen) { - const newChosen = [ ...packagesChosen ]; - newChosen[index].selected = !packagesChosen[index].selected; - setPackagesChosenSorted(newChosen); - } else { - const newAvailable = [ ...packagesAvailable ]; - newAvailable[index].selected = !packagesAvailable[index].selected; - setPackagesAvailableSorted(newAvailable); - } - }; - - const firstInputElement = useRef(null); - - useEffect(() => { - firstInputElement.current?.focus(); - }, []); - - const handleClearAvailableSearch = () => { - setPackagesSearchName(undefined); - setFilterAvailable(undefined); - setPackagesAvailable([]); - setPackagesAvailableFound(true); - }; - - const handleClearChosenSearch = () => { - setFilterChosen(undefined); - setPackagesChosenSorted(packagesChosen.map(pack => { - return { ...pack, isHidden: false };})); - setPackagesChosenFound(true); - }; - - return ( - - setFocus('available') } - onBlur={ () => setFocus('') } - onChange={ (val) => setPackagesSearchName(val) } - submitSearchButtonLabel="Search button for available packages" - onSearch={ handlePackagesAvailableSearch } - resetButtonLabel="Clear available packages search" - onClear={ handleClearAvailableSearch } /> }> - - {!packagesAvailable.length ? ( -

- {!packagesAvailableFound - ? 'No packages found' - : <>Search above to add additional
packages to your image - } -

- ) : (packagesAvailable.map((pack, index) => { - return !pack.isHidden ? ( - onOptionSelect(e, index, false) }> - - { pack.name } - { pack.summary } - - - ) : null; - }))} -
-
- - option.selected) } - onClick={ () => moveSelectedToChosen() } - aria-label="Add selected" - tooltipContent="Add selected"> - - - moveAllToChosen() } - aria-label="Add all" - tooltipContent="Add all"> - - - moveAllToAvailable() } - aria-label="Remove all" - tooltipContent="Remove all"> - - - moveSelectedToAvailable() } - isDisabled={ !packagesChosen.some(option => option.selected) || !packagesChosenFound } - aria-label="Remove selected" - tooltipContent="Remove selected"> - - - - setFocus('chosen') } - onBlur={ () => setFocus('') } - onChange={ (val) => handlePackagesChosenSearch(val) } - resetButtonLabel="Clear chosen packages search" - onClear={ handleClearChosenSearch } /> } - isChosen> - - {!packagesChosen.length ? ( -

- No packages added -

- ) : !packagesChosenFound ? ( -

- No packages found -

- ) : (packagesChosen.map((pack, index) => { - return !pack.isHidden ? ( - onOptionSelect(e, index, true) }> - - { pack.name } - { pack.summary } - - - ) : null; - }))} -
-
-
+ const updateState = (updatedPackagesAvailable, updatedPackagesChosen) => { + setPackagesChosenSorted(updatedPackagesChosen); + setPackagesAvailableSorted(updatedPackagesAvailable); + setPackagesAvailableFound( + areFound(filterAvailable, updatedPackagesAvailable) ); + setPackagesChosenFound(areFound(filterChosen, updatedPackagesChosen)); + // set the steps field to the current chosen packages list + change(input.name, removePackagesDisplayFields(updatedPackagesChosen)); + }; + + const moveSelectedToChosen = () => { + const newPackagesChosen = []; + + const updatedPackagesAvailable = packagesAvailable.filter((pack) => { + if (pack.selected) { + pack.selected = false; + pack.isHidden = isHidden(filterChosen, pack); + newPackagesChosen.push(pack); + return false; + } + + return true; + }); + + const updatedPackagesChosen = [...newPackagesChosen, ...packagesChosen]; + + updateState(updatedPackagesAvailable, updatedPackagesChosen); + }; + + const moveSelectedToAvailable = () => { + const newPackagesAvailable = []; + + const updatedPackagesChosen = packagesChosen.filter((pack) => { + if (pack.selected) { + pack.selected = false; + pack.isHidden = false; + pack.name.includes(filterAvailable) + ? newPackagesAvailable.push(pack) + : null; + return false; + } + + return true; + }); + + const updatedPackagesAvailable = [ + ...newPackagesAvailable, + ...packagesAvailable, + ]; + + updateState(updatedPackagesAvailable, updatedPackagesChosen); + }; + + const moveAllToChosen = () => { + const newPackagesChosen = packagesAvailable.map((pack) => { + return { + ...pack, + selected: false, + isHidden: isHidden(filterChosen, pack), + }; + }); + + const updatedPackagesAvailable = []; + const updatedPackagesChosen = [...newPackagesChosen, ...packagesChosen]; + + updateState(updatedPackagesAvailable, updatedPackagesChosen); + }; + + const moveAllToAvailable = () => { + const updatedPackagesChosen = packagesChosen.filter( + (pack) => pack.isHidden + ); + + const newPackagesAvailable = + filterAvailable === undefined + ? [] + : packagesChosen + .filter( + (pack) => !pack.isHidden && pack.name.includes(filterAvailable) + ) + .map((pack) => { + return { ...pack, selected: false }; + }); + + const updatedPackagesAvailable = [ + ...newPackagesAvailable, + ...packagesAvailable, + ]; + + updateState(updatedPackagesAvailable, updatedPackagesChosen); + }; + + const onOptionSelect = (event, index, isChosen) => { + if (isChosen) { + const newChosen = [...packagesChosen]; + newChosen[index].selected = !packagesChosen[index].selected; + setPackagesChosenSorted(newChosen); + } else { + const newAvailable = [...packagesAvailable]; + newAvailable[index].selected = !packagesAvailable[index].selected; + setPackagesAvailableSorted(newAvailable); + } + }; + + const firstInputElement = useRef(null); + + useEffect(() => { + firstInputElement.current?.focus(); + }, []); + + const handleClearAvailableSearch = () => { + setPackagesSearchName(undefined); + setFilterAvailable(undefined); + setPackagesAvailable([]); + setPackagesAvailableFound(true); + }; + + const handleClearChosenSearch = () => { + setFilterChosen(undefined); + setPackagesChosenSorted( + packagesChosen.map((pack) => { + return { ...pack, isHidden: false }; + }) + ); + setPackagesChosenFound(true); + }; + + return ( + + setFocus('available')} + onBlur={() => setFocus('')} + onChange={(val) => setPackagesSearchName(val)} + submitSearchButtonLabel="Search button for available packages" + onSearch={handlePackagesAvailableSearch} + resetButtonLabel="Clear available packages search" + onClear={handleClearAvailableSearch} + /> + } + > + + {!packagesAvailable.length ? ( +

+ {!packagesAvailableFound ? ( + 'No packages found' + ) : ( + <> + Search above to add additional +
+ packages to your image + + )} +

+ ) : ( + packagesAvailable.map((pack, index) => { + return !pack.isHidden ? ( + onOptionSelect(e, index, false)} + > + + + {pack.name} + + {pack.summary} + + + ) : null; + }) + )} +
+
+ + option.selected)} + onClick={() => moveSelectedToChosen()} + aria-label="Add selected" + tooltipContent="Add selected" + > + + + moveAllToChosen()} + aria-label="Add all" + tooltipContent="Add all" + > + + + moveAllToAvailable()} + aria-label="Remove all" + tooltipContent="Remove all" + > + + + moveSelectedToAvailable()} + isDisabled={ + !packagesChosen.some((option) => option.selected) || + !packagesChosenFound + } + aria-label="Remove selected" + tooltipContent="Remove selected" + > + + + + setFocus('chosen')} + onBlur={() => setFocus('')} + onChange={(val) => handlePackagesChosenSearch(val)} + resetButtonLabel="Clear chosen packages search" + onClear={handleClearChosenSearch} + /> + } + isChosen + > + + {!packagesChosen.length ? ( +

+ No packages added +

+ ) : !packagesChosenFound ? ( +

+ No packages found +

+ ) : ( + packagesChosen.map((pack, index) => { + return !pack.isHidden ? ( + onOptionSelect(e, index, true)} + > + + + {pack.name} + + {pack.summary} + + + ) : null; + }) + )} +
+
+
+ ); }; Packages.propTypes = { - defaultArch: PropTypes.string, + defaultArch: PropTypes.string, }; export default Packages; diff --git a/src/Components/CreateImageWizard/formComponents/RadioWithPopover.js b/src/Components/CreateImageWizard/formComponents/RadioWithPopover.js index 18af03d6..7bd7f839 100644 --- a/src/Components/CreateImageWizard/formComponents/RadioWithPopover.js +++ b/src/Components/CreateImageWizard/formComponents/RadioWithPopover.js @@ -3,15 +3,23 @@ import Radio from '@data-driven-forms/pf4-component-mapper/radio'; import PropTypes from 'prop-types'; const RadioWithPopover = ({ Popover, ...props }) => { - const ref = useRef(); - return {props.label} - - } />; + const ref = useRef(); + return ( + + {props.label} + + + } + /> + ); }; RadioWithPopover.propTypes = { - Popover: PropTypes.elementType.isRequired, - label: PropTypes.node + Popover: PropTypes.elementType.isRequired, + label: PropTypes.node, }; export default RadioWithPopover; diff --git a/src/Components/CreateImageWizard/formComponents/ReviewStep.js b/src/Components/CreateImageWizard/formComponents/ReviewStep.js index 52595ee2..48eefe8a 100644 --- a/src/Components/CreateImageWizard/formComponents/ReviewStep.js +++ b/src/Components/CreateImageWizard/formComponents/ReviewStep.js @@ -1,21 +1,33 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { - Button, - DescriptionList, DescriptionListTerm, DescriptionListGroup, DescriptionListDescription, - List, ListItem, - Popover, - Spinner, - Tabs, Tab, TabTitleText, - Text, TextContent, TextVariants, TextList, TextListVariants, TextListItem, TextListItemVariants, + Button, + DescriptionList, + DescriptionListTerm, + DescriptionListGroup, + DescriptionListDescription, + List, + ListItem, + Popover, + Spinner, + Tabs, + Tab, + TabTitleText, + Text, + TextContent, + TextVariants, + TextList, + TextListVariants, + TextListItem, + TextListItemVariants, } from '@patternfly/react-core'; import { - TableComposable, - Thead, - Tbody, - Tr, - Th, - Td, + TableComposable, + Thead, + Tbody, + Tr, + Th, + Td, } from '@patternfly/react-table'; import { HelpIcon } from '@patternfly/react-icons'; import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; @@ -24,308 +36,386 @@ import { RELEASES, UNIT_GIB, UNIT_MIB } from '../../../constants'; import isRhel from '../../../Utilities/isRhel'; const FSReviewTable = ({ ...props }) => { - return ( - - - - Mount point - Type - Minimum size - - - - {props.fsc.map((r, ri) => - - { r.mountpoint } - xfs - { r.size } { r.unit === UNIT_GIB ? 'GiB' : r.unit === UNIT_MIB ? 'MiB' : 'KiB' } - - )} - - - ); + return ( + + + + Mount point + Type + Minimum size + + + + {props.fsc.map((r, ri) => ( + + {r.mountpoint} + xfs + + {r.size}{' '} + {r.unit === UNIT_GIB + ? 'GiB' + : r.unit === UNIT_MIB + ? 'MiB' + : 'KiB'} + + + ))} + + + ); }; FSReviewTable.propTypes = { - fsc: PropTypes.arrayOf(PropTypes.object).isRequired, + fsc: PropTypes.arrayOf(PropTypes.object).isRequired, }; const ReviewStep = () => { - const [ activeTabKey, setActiveTabKey ] = useState(0); - const [ orgId, setOrgId ] = useState(); - const [ minSize, setMinSize ] = useState(); - const { change, getState } = useFormApi(); + const [activeTabKey, setActiveTabKey] = useState(0); + const [orgId, setOrgId] = useState(); + const [minSize, setMinSize] = useState(); + const { change, getState } = useFormApi(); - useEffect(() => { - const registerSystem = getState()?.values?.['register-system']; - if (registerSystem === 'register-now' || registerSystem === 'register-now-insights') { - (async () => { - const userData = await insights?.chrome?.auth?.getUser(); - const id = userData?.identity?.internal?.org_id; - setOrgId(id); - change('subscription-organization-id', id); - })(); - } + useEffect(() => { + const registerSystem = getState()?.values?.['register-system']; + if ( + registerSystem === 'register-now' || + registerSystem === 'register-now-insights' + ) { + (async () => { + const userData = await insights?.chrome?.auth?.getUser(); + const id = userData?.identity?.internal?.org_id; + setOrgId(id); + change('subscription-organization-id', id); + })(); + } - if (getState()?.values?.['file-system-config-toggle'] === 'manual' && - getState()?.values?.['file-system-configuration']) { - let size = 0; - for (const fsc of getState().values['file-system-configuration']) { - size += (fsc.size * fsc.unit); - } + if ( + getState()?.values?.['file-system-config-toggle'] === 'manual' && + getState()?.values?.['file-system-configuration'] + ) { + let size = 0; + for (const fsc of getState().values['file-system-configuration']) { + size += fsc.size * fsc.unit; + } - size = (size / UNIT_GIB).toFixed(1); - if (size < 1) { - setMinSize(`Less than 1 GiB`); - } else { - setMinSize(`${size} GiB`); - } - } - }); + size = (size / UNIT_GIB).toFixed(1); + if (size < 1) { + setMinSize(`Less than 1 GiB`); + } else { + setMinSize(`${size} GiB`); + } + } + }); - const handleTabClick = (event, tabIndex) => { - setActiveTabKey(tabIndex); - }; + const handleTabClick = (event, tabIndex) => { + setActiveTabKey(tabIndex); + }; - return ( - <> - - Review the information and click "Create image" - to create the image using the following criteria. - - - - {getState()?.values?.['image-name'] && - <> - Image name - - {getState()?.values?.['image-name']} - - - } - Release - - {RELEASES[getState()?.values?.release]} - - - - - Target environment } data-testid='tab-target' autoFocus> - - {getState()?.values?.['target-environment']?.aws && - }> - - - Amazon Web Services - - - Account ID - - {getState()?.values?.['aws-account-id']} - - - - - } - {getState()?.values?.['target-environment']?.gcp && - }> - - Google Cloud Platform - - - {googleAccType?.[getState()?.values?.['google-account-type']]} - - - {getState()?.values?.['google-email'] || getState()?.values?.['google-domain']} - - - - - } - {getState()?.values?.['target-environment']?.azure && - }> - - Microsoft Azure - - - Subscription ID - - - {getState()?.values?.['azure-subscription-id']} - - - Tenant ID - - - {getState()?.values?.['azure-tenant-id']} - - - Resource group - - - {getState()?.values?.['azure-resource-group']} - - - - - } - {getState()?.values?.['target-environment']?.vsphere && - - - - VMWare - - - - } - {getState()?.values?.['target-environment']?.['guest-image'] && - - - - Virtualization - Guest image - - - - } - {getState()?.values?.['target-environment']?.['image-installer'] && - - - - Bare metal - Installer - - - - } - - - {isRhel(getState()?.values?.release) && - Registration } data-testid='tab-registration'> - {getState()?.values?.['register-system'] === 'register-later' && - - - - Subscription - - - Register the system later - - - - } - {(getState()?.values?.['register-system'] === 'register-now' || - getState()?.values?.['register-system'] === 'register-now-insights') && - - - - Subscription - - - {getState()?.values?.['register-system'] === 'register-now-insights' && - 'Register with Subscriptions and Red Hat Insights' - } - {getState()?.values?.['register-system'] === 'register-now' && - 'Register with Subscriptions' - } - - - Activation key - - - {getState()?.values?.['subscription-activation-key']} - - - Organization ID - - {orgId !== undefined ? ( - - {orgId} - - ) : ( - - - - )} - - - } - + return ( + <> + + Review the information and click "Create image" to create the + image using the following criteria. + + + + {getState()?.values?.['image-name'] && ( + <> + Image name + + {getState()?.values?.['image-name']} + + + )} + Release + + {RELEASES[getState()?.values?.release]} + + + + + Target environment} + data-testid="tab-target" + autoFocus + > + + {getState()?.values?.['target-environment']?.aws && ( + } - System configuration } data-testid='tab-system'> - - File system configuration - - - Partitioning - - - {getState()?.values?.['file-system-config-toggle'] === 'manual' ? 'Manual' : 'Automatic'} - {getState()?.values?.['file-system-config-toggle'] === 'manual' && - <> - {' '} - }> - - - - } - - {getState()?.values?.['file-system-config-toggle'] === 'manual' && - <> - - Image size (minimum) - - - Image Builder may extend this size based on requirements, - selected packages, and configurations. - - }> - - - - - { minSize } - - - } - - Packages - - - Chosen - - - {getState()?.values?.['selected-packages']?.length || 0} - - - - - - - ); + > + + Amazon Web Services + + + Account ID + + + {getState()?.values?.['aws-account-id']} + + + + + )} + {getState()?.values?.['target-environment']?.gcp && ( + + } + > + + Google Cloud Platform + + + { + googleAccType?.[ + getState()?.values?.['google-account-type'] + ] + } + + + {getState()?.values?.['google-email'] || + getState()?.values?.['google-domain']} + + + + + )} + {getState()?.values?.['target-environment']?.azure && ( + + } + > + + Microsoft Azure + + + Subscription ID + + + {getState()?.values?.['azure-subscription-id']} + + + Tenant ID + + + {getState()?.values?.['azure-tenant-id']} + + + Resource group + + + {getState()?.values?.['azure-resource-group']} + + + + + )} + {getState()?.values?.['target-environment']?.vsphere && ( + + + VMWare + + + )} + {getState()?.values?.['target-environment']?.['guest-image'] && ( + + + + Virtualization - Guest image + + + + )} + {getState()?.values?.['target-environment']?.[ + 'image-installer' + ] && ( + + + + Bare metal - Installer + + + + )} + + + {isRhel(getState()?.values?.release) && ( + Registration} + data-testid="tab-registration" + > + {getState()?.values?.['register-system'] === 'register-later' && ( + + + + Subscription + + + Register the system later + + + + )} + {(getState()?.values?.['register-system'] === 'register-now' || + getState()?.values?.['register-system'] === + 'register-now-insights') && ( + + + + Subscription + + + {getState()?.values?.['register-system'] === + 'register-now-insights' && + 'Register with Subscriptions and Red Hat Insights'} + {getState()?.values?.['register-system'] === + 'register-now' && 'Register with Subscriptions'} + + + Activation key + + + {getState()?.values?.['subscription-activation-key']} + + + Organization ID + + {orgId !== undefined ? ( + + {orgId} + + ) : ( + + + + )} + + + )} + + )} + System configuration} + data-testid="tab-system" + > + + File system configuration + + + Partitioning + + + {getState()?.values?.['file-system-config-toggle'] === 'manual' + ? 'Manual' + : 'Automatic'} + {getState()?.values?.['file-system-config-toggle'] === + 'manual' && ( + <> + {' '} + + } + > + + + + )} + + {getState()?.values?.['file-system-config-toggle'] === + 'manual' && ( + <> + + Image size (minimum) + + + Image Builder may extend this size based on + requirements, selected packages, and configurations. + + + } + > + + + + + {minSize} + + + )} + + Packages + + + Chosen + + + {getState()?.values?.['selected-packages']?.length || 0} + + + + + + + ); }; export default ReviewStep; diff --git a/src/Components/CreateImageWizard/formComponents/SizeUnit.js b/src/Components/CreateImageWizard/formComponents/SizeUnit.js index 1b9aafdf..71695c4d 100644 --- a/src/Components/CreateImageWizard/formComponents/SizeUnit.js +++ b/src/Components/CreateImageWizard/formComponents/SizeUnit.js @@ -1,74 +1,75 @@ -import React, { - useEffect, - useState, -} from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { - Select, - SelectOption, - SelectVariant, - TextInput, + Select, + SelectOption, + SelectVariant, + TextInput, } from '@patternfly/react-core'; import { UNIT_KIB, UNIT_MIB, UNIT_GIB } from '../../../constants'; const SizeUnit = ({ ...props }) => { - const [ isOpen, setIsOpen ] = useState(false); - const [ unit, setUnit ] = useState(props.unit || UNIT_GIB); - const [ size, setSize ] = useState(props.size || 1); + const [isOpen, setIsOpen] = useState(false); + const [unit, setUnit] = useState(props.unit || UNIT_GIB); + const [size, setSize] = useState(props.size || 1); - useEffect(() => { - props.onChange(size, unit); - }, [ unit, size ]); + useEffect(() => { + props.onChange(size, unit); + }, [unit, size]); - const onToggle = (isOpen) => { - setIsOpen(isOpen); - }; + const onToggle = (isOpen) => { + setIsOpen(isOpen); + }; - const onSelect = (event, selection) => { - switch (selection) { - case 'KiB': - setUnit(UNIT_KIB); - break; - case 'MiB': - setUnit(UNIT_MIB); - break; - case 'GiB': - setUnit(UNIT_GIB); - break; + const onSelect = (event, selection) => { + switch (selection) { + case 'KiB': + setUnit(UNIT_KIB); + break; + case 'MiB': + setUnit(UNIT_MIB); + break; + case 'GiB': + setUnit(UNIT_GIB); + break; + } + + setIsOpen(false); + }; + + return ( + <> + setSize(isNaN(parseInt(v)) ? 0 : parseInt(v))} + /> + - {[ 'KiB', 'MiB', 'GiB' ].map((u, index) => { - return ; - })} - - - ); + variant={SelectVariant.single} + aria-label="Unit select" + > + {['KiB', 'MiB', 'GiB'].map((u, index) => { + return ; + })} + + + ); }; SizeUnit.propTypes = { - size: PropTypes.number.isRequired, - unit: PropTypes.number.isRequired, - onChange: PropTypes.func.isRequired, + size: PropTypes.number.isRequired, + unit: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, }; export default SizeUnit; diff --git a/src/Components/CreateImageWizard/formComponents/TargetEnvironment.js b/src/Components/CreateImageWizard/formComponents/TargetEnvironment.js index 4fa54b0a..930c1c57 100644 --- a/src/Components/CreateImageWizard/formComponents/TargetEnvironment.js +++ b/src/Components/CreateImageWizard/formComponents/TargetEnvironment.js @@ -2,125 +2,164 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api'; import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api'; -import { Checkbox, FormGroup, Text, TextVariants, Tile } from '@patternfly/react-core'; +import { + Checkbox, + FormGroup, + Text, + TextVariants, + Tile, +} from '@patternfly/react-core'; const TargetEnvironment = ({ label, isRequired, ...props }) => { - const { getState, change } = useFormApi(); - const { input } = useFieldApi({ label, isRequired, ...props }); - const [ environment, setEnvironment ] = useState({ - aws: false, - azure: false, - gcp: false, - vsphere: false, - 'guest-image': false, - 'image-installer': false, + const { getState, change } = useFormApi(); + const { input } = useFieldApi({ label, isRequired, ...props }); + const [environment, setEnvironment] = useState({ + aws: false, + azure: false, + gcp: false, + vsphere: false, + 'guest-image': false, + 'image-installer': false, + }); + + useEffect(() => { + if (getState()?.values?.[input.name]) { + setEnvironment(getState().values[input.name]); + } + }, []); + + const handleSetEnvironment = (env) => + setEnvironment((prevEnv) => { + const newEnv = { + ...prevEnv, + [env]: !prevEnv[env], + }; + change(input.name, newEnv); + return newEnv; }); - useEffect(() => { - if (getState()?.values?.[input.name]) { - setEnvironment(getState().values[input.name]); - } - }, []); + const handleKeyDown = (e, env) => { + if (e.key === ' ') { + handleSetEnvironment(env); + } + }; - const handleSetEnvironment = (env) => setEnvironment((prevEnv) => { - const newEnv = ({ - ...prevEnv, - [env]: !prevEnv[env] - }); - change(input.name, newEnv); - return newEnv; - }); - - const handleKeyDown = (e, env) => { - if (e.key === ' ') { - handleSetEnvironment(env); - } - }; - - return ( - <> - - Public cloud } data-testid="target-public"> -
- } - onClick={ () => handleSetEnvironment('aws') } - onKeyDown = { (e) => handleKeyDown(e, 'aws') } - isSelected={ environment.aws } - isStacked - isDisplayLarge /> - } - onClick={ () => handleSetEnvironment('gcp') } - isSelected={ environment.gcp } - onKeyDown = { (e) => handleKeyDown(e, 'gcp') } - isStacked - isDisplayLarge /> - } - onClick={ () => handleSetEnvironment('azure') } - onKeyDown = { (e) => handleKeyDown(e, 'azure') } - isSelected={ environment.azure } - isStacked - isDisplayLarge /> -
-
- Private cloud } data-testid="target-private"> - handleSetEnvironment('vsphere') } - aria-label="VMWare checkbox" - id="checkbox-vmware" - name="VMWare" - data-testid="checkbox-vmware" /> - - Other } data-testid="target-other"> - handleSetEnvironment('guest-image') } - aria-label="Virtualization guest image checkbox" - id="checkbox-guest-image" - name="Virtualization guest image" - data-testid="checkbox-guest-image" /> - handleSetEnvironment('image-installer') } - aria-label="Bare metal installer checkbox" - id="checkbox-image-installer" - name="Bare metal installer" - data-testid="checkbox-image-installer" /> - -
- - ); + return ( + <> + + Public cloud} + data-testid="target-public" + > +
+ + } + onClick={() => handleSetEnvironment('aws')} + onKeyDown={(e) => handleKeyDown(e, 'aws')} + isSelected={environment.aws} + isStacked + isDisplayLarge + /> + + } + onClick={() => handleSetEnvironment('gcp')} + isSelected={environment.gcp} + onKeyDown={(e) => handleKeyDown(e, 'gcp')} + isStacked + isDisplayLarge + /> + + } + onClick={() => handleSetEnvironment('azure')} + onKeyDown={(e) => handleKeyDown(e, 'azure')} + isSelected={environment.azure} + isStacked + isDisplayLarge + /> +
+
+ Private cloud} + data-testid="target-private" + > + handleSetEnvironment('vsphere')} + aria-label="VMWare checkbox" + id="checkbox-vmware" + name="VMWare" + data-testid="checkbox-vmware" + /> + + Other} + data-testid="target-other" + > + handleSetEnvironment('guest-image')} + aria-label="Virtualization guest image checkbox" + id="checkbox-guest-image" + name="Virtualization guest image" + data-testid="checkbox-guest-image" + /> + handleSetEnvironment('image-installer')} + aria-label="Bare metal installer checkbox" + id="checkbox-image-installer" + name="Bare metal installer" + data-testid="checkbox-image-installer" + /> + +
+ + ); }; TargetEnvironment.propTypes = { - label: PropTypes.node, - isRequired: PropTypes.bool + label: PropTypes.node, + isRequired: PropTypes.bool, }; TargetEnvironment.defaultProps = { - label: '', - isRequired: false + label: '', + isRequired: false, }; export default TargetEnvironment; diff --git a/src/Components/CreateImageWizard/steps/aws.js b/src/Components/CreateImageWizard/steps/aws.js index 564d16b9..fc8321e0 100644 --- a/src/Components/CreateImageWizard/steps/aws.js +++ b/src/Components/CreateImageWizard/steps/aws.js @@ -6,40 +6,47 @@ import { Title } from '@patternfly/react-core'; import StepTemplate from './stepTemplate'; export default { - StepTemplate, - id: 'wizard-target-aws', - title: 'Amazon Web Services', - customTitle: Target environment - Amazon Web Service, - name: 'aws-target-env', - substepOf: 'Target environment', - nextStep: ({ values }) => nextStepMapper(values, { skipAws: true }), - fields: [ + StepTemplate, + id: 'wizard-target-aws', + title: 'Amazon Web Services', + customTitle: ( + + Target environment - Amazon Web Service + + ), + name: 'aws-target-env', + substepOf: 'Target environment', + nextStep: ({ values }) => nextStepMapper(values, { skipAws: true }), + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'plain-text-component', + label: ( +

+ Your image will be uploaded to AWS and shared with the account you + provide below.
+ The image should be copied to your account within 14 days. +

+ ), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'aws-account-id', + className: 'pf-u-w-25', + 'data-testid': 'aws-account-id', + type: 'text', + label: 'AWS account ID', + isRequired: true, + autoFocus: true, + validate: [ { - component: componentTypes.PLAIN_TEXT, - name: 'plain-text-component', - label:

- Your image will be uploaded to AWS and shared with the account you provide below.
- The image should be copied to your account within 14 days. -

+ type: validatorTypes.REQUIRED, }, { - component: componentTypes.TEXT_FIELD, - name: 'aws-account-id', - className: 'pf-u-w-25', - 'data-testid': 'aws-account-id', - type: 'text', - label: 'AWS account ID', - isRequired: true, - autoFocus: true, - validate: [ - { - type: validatorTypes.REQUIRED, - }, - { - type: validatorTypes.EXACT_LENGTH, - threshold: 12 - } - ], - } - ] + type: validatorTypes.EXACT_LENGTH, + threshold: 12, + }, + ], + }, + ], }; diff --git a/src/Components/CreateImageWizard/steps/fileSystemConfiguration.js b/src/Components/CreateImageWizard/steps/fileSystemConfiguration.js index eb42d421..87234ee7 100644 --- a/src/Components/CreateImageWizard/steps/fileSystemConfiguration.js +++ b/src/Components/CreateImageWizard/steps/fileSystemConfiguration.js @@ -6,37 +6,46 @@ import { Text } from '@patternfly/react-core'; import StepTemplate from './stepTemplate'; export default { - StepTemplate, - id: 'wizard-systemconfiguration-filesystem', - title: 'File system configuration', - name: 'File system configuration', - substepOf: 'System Configuration', - nextStep: 'packages', - fields: [ - { - component: componentTypes.PLAIN_TEXT, - name: 'file-system-configuration-text-component', - label: <> - Red Hat recommends using automatic partitioning for most installations. - - Alternatively, you may manually configure the file system of your image by adding, removing, and editing partitions. - - , - }, - { - component: 'file-system-config-toggle', - name: 'file-system-config-toggle', - label: 'File system configurations toggle', - }, - { - component: 'file-system-configuration', - name: 'file-system-configuration', - label: 'File system configurations', - validate: [{ type: 'fileSystemConfigurationValidator' }, { type: validatorTypes.REQUIRED }], - condition: { - when: 'file-system-config-toggle', - is: 'manual', - }, - }, - ] + StepTemplate, + id: 'wizard-systemconfiguration-filesystem', + title: 'File system configuration', + name: 'File system configuration', + substepOf: 'System Configuration', + nextStep: 'packages', + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'file-system-configuration-text-component', + label: ( + <> + + Red Hat recommends using automatic partitioning for most + installations. + + + Alternatively, you may manually configure the file system of your + image by adding, removing, and editing partitions. + + + ), + }, + { + component: 'file-system-config-toggle', + name: 'file-system-config-toggle', + label: 'File system configurations toggle', + }, + { + component: 'file-system-configuration', + name: 'file-system-configuration', + label: 'File system configurations', + validate: [ + { type: 'fileSystemConfigurationValidator' }, + { type: validatorTypes.REQUIRED }, + ], + condition: { + when: 'file-system-config-toggle', + is: 'manual', + }, + }, + ], }; diff --git a/src/Components/CreateImageWizard/steps/googleCloud.js b/src/Components/CreateImageWizard/steps/googleCloud.js index b5240b52..7c045f95 100644 --- a/src/Components/CreateImageWizard/steps/googleCloud.js +++ b/src/Components/CreateImageWizard/steps/googleCloud.js @@ -3,136 +3,168 @@ import componentTypes from '@data-driven-forms/react-form-renderer/component-typ import validatorTypes from '@data-driven-forms/react-form-renderer/validator-types'; import { HelpIcon } from '@patternfly/react-icons'; import nextStepMapper from './imageOutputStepMapper'; -import { Title, Text, Popover, TextContent, TextList, TextListItem, Button } from '@patternfly/react-core'; +import { + Title, + Text, + Popover, + TextContent, + TextList, + TextListItem, + Button, +} from '@patternfly/react-core'; import PropTypes from 'prop-types'; import StepTemplate from './stepTemplate'; export const googleAccType = { - googleAccount: 'Google account', - serviceAccount: 'Service account', - googleGroup: 'Google group', - domain: 'Domain' + googleAccount: 'Google account', + serviceAccount: 'Service account', + googleGroup: 'Google group', + domain: 'Domain', }; const PopoverInfo = ({ appendTo }) => { - return - The following account types can have an image shared with them: - - - Google account: A Google account represents a developer, an administrator, - or any other person who interacts with Google Cloud. e.g., `alice@gmail.com`. - - - Service account: A service account is an account for an application instead - of an individual end user. e.g., `myapp@appspot.gserviceaccount.com`. - - - Google group: A Google group is a named collection of Google accounts and - service accounts. e.g., `admins@example.com`. - - - Google Workspace domain/Cloud Identity domain: A Google workspace or cloud identity - domain represents a virtual group of all the Google accounts in an organization. These domains - represent your organization's internet domain name. e.g., `mycompany.com`. - - - }> - - ; + return ( + + + The following account types can have an image shared with them: + + + + Google account: A Google account represents a + developer, an administrator, or any other person who interacts + with Google Cloud. e.g., `alice@gmail.com`. + + + Service account: A service account is an account + for an application instead of an individual end user. e.g.,{' '} + `myapp@appspot.gserviceaccount.com`. + + + Google group: A Google group is a named + collection of Google accounts and service accounts. e.g.,{' '} + `admins@example.com`. + + + Google Workspace domain/Cloud Identity domain: A + Google workspace or cloud identity domain represents a virtual + group of all the Google accounts in an organization. These domains + represent your organization's internet domain name. e.g.,{' '} + `mycompany.com`. + + + + } + > + + + ); }; PopoverInfo.propTypes = { - appendTo: PropTypes.any + appendTo: PropTypes.any, }; export default { - StepTemplate, - id: 'wizard-target-gcp', - title: 'Google Cloud Platform', - customTitle: Target environment - Google Cloud Platform, - name: 'google-cloud-target-env', - substepOf: 'Target environment', - nextStep: ({ values }) => nextStepMapper(values, { skipGoogle: true, skipAws: true }), - fields: [ + StepTemplate, + id: 'wizard-target-gcp', + title: 'Google Cloud Platform', + customTitle: ( + + Target environment - Google Cloud Platform + + ), + name: 'google-cloud-target-env', + substepOf: 'Target environment', + nextStep: ({ values }) => + nextStepMapper(values, { skipGoogle: true, skipAws: true }), + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'google-cloud-text-component', + label: ( + + Your image will be uploaded to Google Cloud Platform and shared with + the email you provide below.
+ The image should be copied to your account within 14 days. +
+ ), + }, + { + component: 'radio-popover', + label: 'Type', + isRequired: true, + Popover: PopoverInfo, + name: 'google-account-type', + initialValue: 'googleAccount', + options: Object.entries(googleAccType).map(([value, label]) => ({ + label: + value === 'domain' + ? 'Google Workspace domain or Cloud Identity domain' + : label, + value, + autoFocus: value === 'googleAccount' ? true : false, + })), + validate: [ { - component: componentTypes.PLAIN_TEXT, - name: 'google-cloud-text-component', - label: - Your image will be uploaded to Google Cloud Platform and shared with the email you provide below.
- The image should be copied to your account within 14 days. -
+ type: validatorTypes.REQUIRED, + }, + ], + }, + { + component: componentTypes.TEXT_FIELD, + name: 'google-email', + 'data-testid': 'input-google-email', + type: 'text', + label: 'Email address', + condition: { + or: [ + { when: 'google-account-type', is: 'googleAccount' }, + { when: 'google-account-type', is: 'serviceAccount' }, + { when: 'google-account-type', is: 'googleGroup' }, + { when: 'google-account-type', is: null }, + ], + }, + isRequired: true, + validate: [ + { + type: validatorTypes.REQUIRED, }, { - component: 'radio-popover', - label: 'Type', - isRequired: true, - Popover: PopoverInfo, - name: 'google-account-type', - initialValue: 'googleAccount', - options: Object.entries(googleAccType).map(([ value, label ]) => ({ - label: value === 'domain' ? 'Google Workspace domain or Cloud Identity domain' : label, - value, - autoFocus: value === 'googleAccount' ? true : false - })), - validate: [ - { - type: validatorTypes.REQUIRED, - }, - ], + type: validatorTypes.PATTERN, + pattern: '[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,}$', + message: 'Please enter a valid email address', }, + ], + }, + { + component: componentTypes.TEXT_FIELD, + name: 'google-domain', + type: 'text', + label: 'Domain', + condition: { + when: 'google-account-type', + is: 'domain', + }, + isRequired: true, + validate: [ { - component: componentTypes.TEXT_FIELD, - name: 'google-email', - 'data-testid': 'input-google-email', - type: 'text', - label: 'Email address', - condition: { - or: [ - { when: 'google-account-type', is: 'googleAccount' }, - { when: 'google-account-type', is: 'serviceAccount' }, - { when: 'google-account-type', is: 'googleGroup' }, - { when: 'google-account-type', is: null }, - ] - }, - isRequired: true, - validate: [ - { - type: validatorTypes.REQUIRED, - }, - { - type: validatorTypes.PATTERN, - pattern: '[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,}$', - message: 'Please enter a valid email address' - } - ], + type: validatorTypes.REQUIRED, }, - { - component: componentTypes.TEXT_FIELD, - name: 'google-domain', - type: 'text', - label: 'Domain', - condition: { - when: 'google-account-type', - is: 'domain' - }, - isRequired: true, - validate: [ - { - type: validatorTypes.REQUIRED, - }, - ], - } - ] + ], + }, + ], }; diff --git a/src/Components/CreateImageWizard/steps/imageName.js b/src/Components/CreateImageWizard/steps/imageName.js index 557d652b..7eff1c31 100644 --- a/src/Components/CreateImageWizard/steps/imageName.js +++ b/src/Components/CreateImageWizard/steps/imageName.js @@ -4,29 +4,33 @@ import validatorTypes from '@data-driven-forms/react-form-renderer/validator-typ import StepTemplate from './stepTemplate'; export default { - StepTemplate, - id: 'wizard-details', - name: 'image-name', - title: 'Name image', - nextStep: 'review', - fields: [ + StepTemplate, + id: 'wizard-details', + name: 'image-name', + title: 'Name image', + nextStep: 'review', + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'plain-text-component', + label: ( +

+ Optionally enter a name for your image. All images will have a UUID. +

+ ), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'image-name', + type: 'text', + label: 'Image name', + autoFocus: true, + validate: [ { - component: componentTypes.PLAIN_TEXT, - name: 'plain-text-component', - label:

Optionally enter a name for your image. All images will have a UUID.

+ type: validatorTypes.MAX_LENGTH, + threshold: 100, }, - { - component: componentTypes.TEXT_FIELD, - name: 'image-name', - type: 'text', - label: 'Image name', - autoFocus: true, - validate: [ - { - type: validatorTypes.MAX_LENGTH, - threshold: 100 - } - ], - } - ] + ], + }, + ], }; diff --git a/src/Components/CreateImageWizard/steps/imageOutput.js b/src/Components/CreateImageWizard/steps/imageOutput.js index dacf417e..0041db2f 100644 --- a/src/Components/CreateImageWizard/steps/imageOutput.js +++ b/src/Components/CreateImageWizard/steps/imageOutput.js @@ -8,42 +8,49 @@ import DocumentationButton from '../../sharedComponents/DocumentationButton'; import StepTemplate from './stepTemplate'; export default { - StepTemplate, - id: 'wizard-imageoutput', - title: 'Image output', - name: 'image-output', - nextStep: ({ values }) => nextStepMapper(values), - fields: [ + StepTemplate, + id: 'wizard-imageoutput', + title: 'Image output', + name: 'image-output', + nextStep: ({ values }) => nextStepMapper(values), + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'image-output-plain-text', + label: ( + + Image builder allows you to create a custom image and push it to + target environments. +
+ +
+ ), + }, + { + component: 'image-output-release-select', + label: 'Release', + name: 'release', + initialValue: RHEL_9, + isRequired: true, + validate: [ { - component: componentTypes.PLAIN_TEXT, - name: 'image-output-plain-text', - label: Image builder allows you to create a custom image and push it to target environments.
+ type: validatorTypes.REQUIRED, + }, + ], + }, + { + component: 'output', + name: 'target-environment', + label: 'Select target environments', + isRequired: true, + validate: [ + { + type: validatorTypes.REQUIRED, }, { - component: 'image-output-release-select', - label: 'Release', - name: 'release', - initialValue: RHEL_9, - isRequired: true, - validate: [ - { - type: validatorTypes.REQUIRED - } - ], + type: 'targetEnvironmentValidator', }, - { - component: 'output', - name: 'target-environment', - label: 'Select target environments', - isRequired: true, - validate: [ - { - type: validatorTypes.REQUIRED - }, - { - type: 'targetEnvironmentValidator' - } - ], - } - ] + ], + }, + ], }; diff --git a/src/Components/CreateImageWizard/steps/imageOutputStepMapper.js b/src/Components/CreateImageWizard/steps/imageOutputStepMapper.js index 5e5018d2..1df86a9d 100644 --- a/src/Components/CreateImageWizard/steps/imageOutputStepMapper.js +++ b/src/Components/CreateImageWizard/steps/imageOutputStepMapper.js @@ -1,17 +1,20 @@ import isRhel from '../../../Utilities/isRhel.js'; -export default ({ 'target-environment': targetEnv, release } = {}, { skipAws, skipGoogle, skipAzure } = {}) => { - if (!skipAws && targetEnv?.aws) { - return 'aws-target-env'; - } +export default ( + { 'target-environment': targetEnv, release } = {}, + { skipAws, skipGoogle, skipAzure } = {} +) => { + if (!skipAws && targetEnv?.aws) { + return 'aws-target-env'; + } - if (!skipGoogle && targetEnv?.gcp) { - return 'google-cloud-target-env'; - } + if (!skipGoogle && targetEnv?.gcp) { + return 'google-cloud-target-env'; + } - if (!skipAzure && targetEnv?.azure) { - return 'ms-azure-target-env'; - } + if (!skipAzure && targetEnv?.azure) { + return 'ms-azure-target-env'; + } - return isRhel(release) ? 'registration' : 'File system configuration'; + return isRhel(release) ? 'registration' : 'File system configuration'; }; diff --git a/src/Components/CreateImageWizard/steps/msAzure.js b/src/Components/CreateImageWizard/steps/msAzure.js index 0877cdd5..88672097 100644 --- a/src/Components/CreateImageWizard/steps/msAzure.js +++ b/src/Components/CreateImageWizard/steps/msAzure.js @@ -6,103 +6,119 @@ import nextStepMapper from './imageOutputStepMapper'; import StepTemplate from './stepTemplate'; export default { - StepTemplate, - id: 'wizard-target-msazure', - title: 'Microsoft Azure', - customTitle: Target environment - Microsoft Azure, - name: 'ms-azure-target-env', - substepOf: 'Target environment', - nextStep: ({ values }) => nextStepMapper(values, { skipAws: true, skipGoogle: true, skipAzure: true }), - fields: [ + StepTemplate, + id: 'wizard-target-msazure', + title: 'Microsoft Azure', + customTitle: ( + + Target environment - Microsoft Azure + + ), + name: 'ms-azure-target-env', + substepOf: 'Target environment', + nextStep: ({ values }) => + nextStepMapper(values, { + skipAws: true, + skipGoogle: true, + skipAzure: true, + }), + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'azure-description', + label: ( + + Image Builder sends an image to an authorized Azure account. + + ), + }, + { + component: 'azure-auth-expandable', + name: 'azure-auth-expandable', + }, + { + component: componentTypes.PLAIN_TEXT, + name: 'azure-destination', + label: ( + <> + Destination + + Your image will be uploaded to the resource group in the + subscription you specify. + + + ), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'azure-tenant-id', + className: 'pf-u-w-50', + 'data-testid': 'azure-tenant-id', + type: 'text', + label: 'Tenant ID', + required: true, + isRequired: true, + autoFocus: true, + validate: [ { - component: componentTypes.PLAIN_TEXT, - name: 'azure-description', - label: - Image Builder sends an image to an authorized Azure account. - + type: validatorTypes.REQUIRED, }, { - component: 'azure-auth-expandable', - name: 'azure-auth-expandable' + type: validatorTypes.PATTERN, + pattern: + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + message: 'Please enter a valid tenant ID', + }, + ], + }, + { + component: 'azure-auth-button', + name: 'azure-auth-button', + 'data-testid': 'azure-auth-button', + required: true, + isRequired: true, + }, + { + component: componentTypes.TEXT_FIELD, + name: 'azure-subscription-id', + className: 'pf-u-w-50', + 'data-testid': 'azure-subscription-id', + type: 'text', + label: 'Subscription ID', + isRequired: true, + validate: [ + { + type: validatorTypes.REQUIRED, }, { - component: componentTypes.PLAIN_TEXT, - name: 'azure-destination', - label: <> - Destination - - Your image will be uploaded to the resource group in the subscription you specify. - - - + type: validatorTypes.PATTERN, + pattern: + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + message: 'Please enter a valid subscription ID', + }, + ], + }, + { + component: componentTypes.TEXT_FIELD, + name: 'azure-resource-group', + className: 'pf-u-w-50', + 'data-testid': 'azure-resource-group', + type: 'text', + label: 'Resource group', + isRequired: true, + validate: [ + { + type: validatorTypes.REQUIRED, }, { - component: componentTypes.TEXT_FIELD, - name: 'azure-tenant-id', - className: 'pf-u-w-50', - 'data-testid': 'azure-tenant-id', - type: 'text', - label: 'Tenant ID', - required: true, - isRequired: true, - autoFocus: true, - validate: [ - { - type: validatorTypes.REQUIRED, - }, - { - type: validatorTypes.PATTERN, - pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - message: 'Please enter a valid tenant ID', - } - ], + type: validatorTypes.PATTERN, + pattern: /^[-\w._()]+[-\w_()]$/, + message: + 'Resource group names only allow alphanumeric characters, ' + + 'periods, underscores, hyphens, and parenthesis and cannot end in a period', }, - { - component: 'azure-auth-button', - name: 'azure-auth-button', - 'data-testid': 'azure-auth-button', - required: true, - isRequired: true, - }, - { - component: componentTypes.TEXT_FIELD, - name: 'azure-subscription-id', - className: 'pf-u-w-50', - 'data-testid': 'azure-subscription-id', - type: 'text', - label: 'Subscription ID', - isRequired: true, - validate: [ - { - type: validatorTypes.REQUIRED, - }, - { - type: validatorTypes.PATTERN, - pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - message: 'Please enter a valid subscription ID', - }, - ], - }, - { - component: componentTypes.TEXT_FIELD, - name: 'azure-resource-group', - className: 'pf-u-w-50', - 'data-testid': 'azure-resource-group', - type: 'text', - label: 'Resource group', - isRequired: true, - validate: [ - { - type: validatorTypes.REQUIRED, - }, - { - type: validatorTypes.PATTERN, - pattern: /^[-\w._()]+[-\w_()]$/, - message: 'Resource group names only allow alphanumeric characters, ' + - 'periods, underscores, hyphens, and parenthesis and cannot end in a period', - }, - ], - } - // TODO check oauth2 thing too here? - ] + ], + }, + // TODO check oauth2 thing too here? + ], }; diff --git a/src/Components/CreateImageWizard/steps/packages.js b/src/Components/CreateImageWizard/steps/packages.js index 362a9ff8..16857ffd 100644 --- a/src/Components/CreateImageWizard/steps/packages.js +++ b/src/Components/CreateImageWizard/steps/packages.js @@ -4,22 +4,27 @@ import { Text } from '@patternfly/react-core'; import StepTemplate from './stepTemplate'; export default { - StepTemplate, - id: 'wizard-systemconfiguration-packages', - title: 'Packages', - name: 'packages', - substepOf: 'System Configuration', - nextStep: 'image-name', - fields: [ - { - component: componentTypes.PLAIN_TEXT, - name: 'packages-text-component', - label: Add optional additional packages to your image by searching available packages. - }, - { - component: 'package-selector', - name: 'selected-packages', - label: 'Available options' - } - ] + StepTemplate, + id: 'wizard-systemconfiguration-packages', + title: 'Packages', + name: 'packages', + substepOf: 'System Configuration', + nextStep: 'image-name', + fields: [ + { + component: componentTypes.PLAIN_TEXT, + name: 'packages-text-component', + label: ( + + Add optional additional packages to your image by searching available + packages. + + ), + }, + { + component: 'package-selector', + name: 'selected-packages', + label: 'Available options', + }, + ], }; diff --git a/src/Components/CreateImageWizard/steps/registration.js b/src/Components/CreateImageWizard/steps/registration.js index 0745c196..86ab641d 100644 --- a/src/Components/CreateImageWizard/steps/registration.js +++ b/src/Components/CreateImageWizard/steps/registration.js @@ -1,133 +1,142 @@ import React from 'react'; import componentTypes from '@data-driven-forms/react-form-renderer/component-types'; import validatorTypes from '@data-driven-forms/react-form-renderer/validator-types'; -import { Button, Popover, Text, TextContent, TextVariants } from '@patternfly/react-core'; +import { + Button, + Popover, + Text, + TextContent, + TextVariants, +} from '@patternfly/react-core'; import { ExternalLinkAltIcon, HelpIcon } from '@patternfly/react-icons'; import StepTemplate from './stepTemplate'; const PopoverActivation = () => { - return - - Activation keys allow you to register a system with - appropriate subscriptions and system purpose attached. - - }> - - ; + return ( + + + Activation keys allow you to register a system with appropriate + subscriptions and system purpose attached. + + + } + > + + + ); }; export default { - StepTemplate, - id: 'wizard-registration', - title: 'Registration', - name: 'registration', - nextStep: 'File system configuration', - fields: [ + StepTemplate, + id: 'wizard-registration', + title: 'Registration', + name: 'registration', + nextStep: 'File system configuration', + fields: [ + { + component: componentTypes.RADIO, + label: 'Register images with Red Hat', + name: 'register-system', + initialValue: 'register-now-insights', + options: [ { - component: componentTypes.RADIO, - label: 'Register images with Red Hat', - name: 'register-system', - initialValue: 'register-now-insights', - options: [ - { - label: 'Register and connect image instances with Red Hat', - description: 'Includes Subscriptions and Red Hat Insights', - value: 'register-now-insights', - 'data-testid': 'radio-register-now-insights', - autoFocus: true, - }, - { - label: 'Register image instances only', - description: 'Includes Subscriptions only', - value: 'register-now', - className: 'pf-u-mt-sm', - 'data-testid': 'radio-register-now', - }, - { - label: 'Register later', - value: 'register-later', - className: 'pf-u-mt-sm', - 'data-testid': 'radio-register-later', - }, - ] + label: 'Register and connect image instances with Red Hat', + description: 'Includes Subscriptions and Red Hat Insights', + value: 'register-now-insights', + 'data-testid': 'radio-register-now-insights', + autoFocus: true, }, { - component: 'activation-keys', - name: 'subscription-activation-key', - required: true, - label: ( - <> - Activation key to use for this image - - - ), - condition: { - or: [ - { when: 'register-system', is: 'register-now-insights' }, - { when: 'register-system', is: 'register-now' }, - ] - }, - isRequired: true, - validate: [ - { - type: validatorTypes.REQUIRED, - }, - ], + label: 'Register image instances only', + description: 'Includes Subscriptions only', + value: 'register-now', + className: 'pf-u-mt-sm', + 'data-testid': 'radio-register-now', }, { - component: componentTypes.PLAIN_TEXT, - name: 'subscription-activation-description', - label: ( - <> - Create and manage activation keys in the  - - - ), - condition: { - or: [ - { when: 'register-system', is: 'register-now-insights' }, - { when: 'register-system', is: 'register-now' }, - ] - }, + label: 'Register later', + value: 'register-later', + className: 'pf-u-mt-sm', + 'data-testid': 'radio-register-later', }, + ], + }, + { + component: 'activation-keys', + name: 'subscription-activation-key', + required: true, + label: ( + <> + Activation key to use for this image + + + ), + condition: { + or: [ + { when: 'register-system', is: 'register-now-insights' }, + { when: 'register-system', is: 'register-now' }, + ], + }, + isRequired: true, + validate: [ { - component: componentTypes.PLAIN_TEXT, - name: 'subscription-register-later', - label: ( - - Register Later - - On initial boot, systems will need to be registered manually - before having access to updates or Red Hat services. - - - Registering now is recommended. - - - ), - condition: { - or: [ - { when: 'register-system', is: 'register-later' }, - ] - }, - } - ] + type: validatorTypes.REQUIRED, + }, + ], + }, + { + component: componentTypes.PLAIN_TEXT, + name: 'subscription-activation-description', + label: ( + <> + Create and manage activation keys in the  + + + ), + condition: { + or: [ + { when: 'register-system', is: 'register-now-insights' }, + { when: 'register-system', is: 'register-now' }, + ], + }, + }, + { + component: componentTypes.PLAIN_TEXT, + name: 'subscription-register-later', + label: ( + + Register Later + + On initial boot, systems will need to be registered manually before + having access to updates or Red Hat services. + + Registering now is recommended. + + ), + condition: { + or: [{ when: 'register-system', is: 'register-later' }], + }, + }, + ], }; diff --git a/src/Components/CreateImageWizard/steps/review.js b/src/Components/CreateImageWizard/steps/review.js index f28dcf0d..ab20c921 100644 --- a/src/Components/CreateImageWizard/steps/review.js +++ b/src/Components/CreateImageWizard/steps/review.js @@ -2,15 +2,15 @@ import CustomButtons from '../formComponents/CustomSubmitButtons'; import StepTemplate from './stepTemplate'; export default { - StepTemplate, - id: 'wizard-review', - name: 'review', - title: 'Review', - buttons: CustomButtons, - fields: [ - { - name: 'review', - component: 'review' - } - ] + StepTemplate, + id: 'wizard-review', + name: 'review', + title: 'Review', + buttons: CustomButtons, + fields: [ + { + name: 'review', + component: 'review', + }, + ], }; diff --git a/src/Components/CreateImageWizard/steps/stepTemplate.js b/src/Components/CreateImageWizard/steps/stepTemplate.js index d7412561..f05656e8 100644 --- a/src/Components/CreateImageWizard/steps/stepTemplate.js +++ b/src/Components/CreateImageWizard/steps/stepTemplate.js @@ -2,31 +2,42 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Title } from '@patternfly/react-core'; -const StepTemplate = ({ id, formFields, formRef, title, customTitle, showTitle, showTitles }) => ( -
- {((showTitles && showTitle !== false) || showTitle) && - (customTitle ? ( - customTitle - ) : ( - - {title} - - ))} - {formFields} -
+const StepTemplate = ({ + id, + formFields, + formRef, + title, + customTitle, + showTitle, + showTitles, +}) => ( +
+ {((showTitles && showTitle !== false) || showTitle) && + (customTitle ? ( + customTitle + ) : ( + + {title} + + ))} + {formFields} +
); StepTemplate.propTypes = { - id: PropTypes.string, - title: PropTypes.node, - customTitle: PropTypes.node, - formFields: PropTypes.array.isRequired, - formOptions: PropTypes.shape({ - renderForm: PropTypes.func.isRequired, - }).isRequired, - showTitles: PropTypes.bool, - showTitle: PropTypes.bool, - formRef: PropTypes.oneOfType([ PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) }) ]), + id: PropTypes.string, + title: PropTypes.node, + customTitle: PropTypes.node, + formFields: PropTypes.array.isRequired, + formOptions: PropTypes.shape({ + renderForm: PropTypes.func.isRequired, + }).isRequired, + showTitles: PropTypes.bool, + showTitle: PropTypes.bool, + formRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), }; export default StepTemplate; diff --git a/src/Components/CreateImageWizard/validators/fileSystemConfigurationValidator.js b/src/Components/CreateImageWizard/validators/fileSystemConfigurationValidator.js index 8d4a5ed9..85c4cc96 100644 --- a/src/Components/CreateImageWizard/validators/fileSystemConfigurationValidator.js +++ b/src/Components/CreateImageWizard/validators/fileSystemConfigurationValidator.js @@ -1,30 +1,32 @@ -const FileSystemConfigurationValidator = () => fsc => { - if (!fsc) { - return undefined; - } +const FileSystemConfigurationValidator = () => (fsc) => { + if (!fsc) { + return undefined; + } - let mpFreqs = {}; - for (const fs of fsc) { - const mp = fs.mountpoint; - if (mp in mpFreqs) { - mpFreqs[mp]++; - } else { - mpFreqs[mp] = 1; - } + let mpFreqs = {}; + for (const fs of fsc) { + const mp = fs.mountpoint; + if (mp in mpFreqs) { + mpFreqs[mp]++; + } else { + mpFreqs[mp] = 1; } + } - let duplicates = []; - for (const [ k, v ] of Object.entries(mpFreqs)) { - if (v > 1) { - duplicates.push(k); - } + let duplicates = []; + for (const [k, v] of Object.entries(mpFreqs)) { + if (v > 1) { + duplicates.push(k); } + } - let root = mpFreqs['/'] >= 1; - return duplicates.length === 0 && root ? undefined : { + let root = mpFreqs['/'] >= 1; + return duplicates.length === 0 && root + ? undefined + : { duplicates: duplicates === [] ? undefined : duplicates, root, - }; + }; }; export default FileSystemConfigurationValidator; diff --git a/src/Components/CreateImageWizard/validators/targetEnvironmentValidator.js b/src/Components/CreateImageWizard/validators/targetEnvironmentValidator.js index e56be5c2..459a51a2 100644 --- a/src/Components/CreateImageWizard/validators/targetEnvironmentValidator.js +++ b/src/Components/CreateImageWizard/validators/targetEnvironmentValidator.js @@ -1,14 +1,17 @@ -const TargetEnvironmentValidator = () => targets => { - if (!targets) { - return undefined; - } +const TargetEnvironmentValidator = () => (targets) => { + if (!targets) { + return undefined; + } - // at least one of the target environments must - // be set to true. This reduces the value to - // a single boolean which is a flag for whether - // at least one target has been selected or not - let valid = Object.values(targets).reduce((prev, curr) => curr || prev, false); - return !valid ? 'Please select an image' : undefined; + // at least one of the target environments must + // be set to true. This reduces the value to + // a single boolean which is a flag for whether + // at least one target has been selected or not + let valid = Object.values(targets).reduce( + (prev, curr) => curr || prev, + false + ); + return !valid ? 'Please select an image' : undefined; }; export default TargetEnvironmentValidator; diff --git a/src/Components/ImagesTable/ImageBuildErrorDetails.js b/src/Components/ImagesTable/ImageBuildErrorDetails.js index a853a24f..4d79be34 100644 --- a/src/Components/ImagesTable/ImageBuildErrorDetails.js +++ b/src/Components/ImagesTable/ImageBuildErrorDetails.js @@ -3,35 +3,35 @@ import PropTypes from 'prop-types'; import { Alert } from '@patternfly/react-core'; const useGetErrorReason = (err) => { - if (!err?.reason) { - return 'An unknown error occured'; - } + if (!err?.reason) { + return 'An unknown error occured'; + } - if (err.details?.reason) { - return err.details.reason; - } + if (err.details?.reason) { + return err.details.reason; + } - return err.reason; + return err.reason; }; const ErrorDetails = ({ status }) => { - if (!status || status.status !== 'failure') { - return <>; - } + if (!status || status.status !== 'failure') { + return <>; + } - const reason = useGetErrorReason(status.error); + const reason = useGetErrorReason(status.error); - return ( -
- Status - -

{reason}

-
- ); + return ( +
+ Status + +

{reason}

+
+ ); }; ErrorDetails.propTypes = { - status: PropTypes.object, + status: PropTypes.object, }; export default ErrorDetails; diff --git a/src/Components/ImagesTable/ImageBuildStatus.js b/src/Components/ImagesTable/ImageBuildStatus.js index aa962f01..fab7cad0 100644 --- a/src/Components/ImagesTable/ImageBuildStatus.js +++ b/src/Components/ImagesTable/ImageBuildStatus.js @@ -2,72 +2,76 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Flex } from '@patternfly/react-core'; -import { CheckCircleIcon, PendingIcon, ExclamationCircleIcon, InProgressIcon } from '@patternfly/react-icons'; +import { + CheckCircleIcon, + PendingIcon, + ExclamationCircleIcon, + InProgressIcon, +} from '@patternfly/react-icons'; import './ImageBuildStatus.scss'; const ImageBuildStatus = (props) => { - const messages = { - success: [ - { - icon: , - text: 'Ready' - } - ], - failure: [ - { - icon: , - text: 'Image build failed' - } - ], - pending: [ - { - icon: , - text: 'Image build is pending' - } - ], - // Keep "running" for backward compatibility - running: [ - { - icon: , - text: 'Image build in progress' - } - ], - building: [ - { - icon: , - text: 'Image build in progress' - } - ], - uploading: [ - { - icon: , - text: 'Image upload in progress' - } - ], - registering: [ - { - icon: , - text: 'Cloud registration in progress' - } - ] - }; - return ( - - {messages[props.status] && - messages[props.status].map((message, key) => ( - -
{message.icon}
- {message.text} -
- )) - } -
- ); + const messages = { + success: [ + { + icon: , + text: 'Ready', + }, + ], + failure: [ + { + icon: , + text: 'Image build failed', + }, + ], + pending: [ + { + icon: , + text: 'Image build is pending', + }, + ], + // Keep "running" for backward compatibility + running: [ + { + icon: , + text: 'Image build in progress', + }, + ], + building: [ + { + icon: , + text: 'Image build in progress', + }, + ], + uploading: [ + { + icon: , + text: 'Image upload in progress', + }, + ], + registering: [ + { + icon: , + text: 'Cloud registration in progress', + }, + ], + }; + return ( + + {messages[props.status] && + messages[props.status].map((message, key) => ( + +
{message.icon}
+ {message.text} +
+ ))} +
+ ); }; ImageBuildStatus.propTypes = { - status: PropTypes.string, + status: PropTypes.string, }; export default ImageBuildStatus; diff --git a/src/Components/ImagesTable/ImageLink.js b/src/Components/ImagesTable/ImageLink.js index 364c7127..acf7dd29 100644 --- a/src/Components/ImagesTable/ImageLink.js +++ b/src/Components/ImagesTable/ImageLink.js @@ -1,112 +1,129 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, TextContent, Text, TextVariants, Popover } from '@patternfly/react-core'; +import { + Button, + TextContent, + Text, + TextVariants, + Popover, +} from '@patternfly/react-core'; import { DownloadIcon, ExternalLinkAltIcon } from '@patternfly/react-icons'; const ImageLink = (props) => { - const fileExtensions = { - vsphere: '.vmdk', - 'guest-image': '.qcow2', - 'image-installer': '.iso', - }; + const fileExtensions = { + vsphere: '.vmdk', + 'guest-image': '.qcow2', + 'image-installer': '.iso', + }; - const uploadStatus = props.imageStatus ? props.imageStatus.upload_status : undefined; - if (uploadStatus) { - if (uploadStatus.type === 'aws') { - const url = 'https://console.aws.amazon.com/ec2/v2/home?region=' + - uploadStatus.options.region + - '#LaunchInstanceWizard:ami=' + - uploadStatus.options.ami; - return ( - - ); - } else if (uploadStatus.type === 'azure') { - const url = 'https://portal.azure.com/#@' + props.uploadOptions.tenant_id + - '/resource/subscriptions/' + props.uploadOptions.subscription_id + - '/resourceGroups/' + props.uploadOptions.resource_group + - '/providers/Microsoft.Compute/images/' + uploadStatus.options.image_name; - return ( - - ); - } else if (uploadStatus.type === 'gcp') { - return ( - - - To use an Image Builder created Google Cloud Platform (GCP) image in your project, - specify the project ID and image name in your templates and configurations. - - - Project ID -
- {uploadStatus.options.project_id} -
- - Image Name -
- {uploadStatus.options.image_name} -
- - Shared with -
- {/* the account the image is shared with is stored in the form type:account so this extracts the account */} - {props.uploadOptions.share_with_accounts[0].split(':')[1]} -
- }> - -
- ); - } else if (uploadStatus.type === 'aws.s3') { - return ( - - ); - } + const uploadStatus = props.imageStatus + ? props.imageStatus.upload_status + : undefined; + if (uploadStatus) { + if (uploadStatus.type === 'aws') { + const url = + 'https://console.aws.amazon.com/ec2/v2/home?region=' + + uploadStatus.options.region + + '#LaunchInstanceWizard:ami=' + + uploadStatus.options.ami; + return ( + + ); + } else if (uploadStatus.type === 'azure') { + const url = + 'https://portal.azure.com/#@' + + props.uploadOptions.tenant_id + + '/resource/subscriptions/' + + props.uploadOptions.subscription_id + + '/resourceGroups/' + + props.uploadOptions.resource_group + + '/providers/Microsoft.Compute/images/' + + uploadStatus.options.image_name; + return ( + + ); + } else if (uploadStatus.type === 'gcp') { + return ( + + + To use an Image Builder created Google Cloud Platform (GCP) + image in your project, specify the project ID and image name in + your templates and configurations. + + + Project ID +
+ {uploadStatus.options.project_id} +
+ + Image Name +
+ {uploadStatus.options.image_name} +
+ + Shared with +
+ {/* the account the image is shared with is stored in the form type:account so this extracts the account */} + {props.uploadOptions.share_with_accounts[0].split(':')[1]} +
+ + } + > + +
+ ); + } else if (uploadStatus.type === 'aws.s3') { + return ( + + ); } + } - return null; + return null; }; ImageLink.propTypes = { - imageStatus: PropTypes.object, - imageType: PropTypes.string, - uploadOptions: PropTypes.object, + imageStatus: PropTypes.object, + imageType: PropTypes.string, + uploadOptions: PropTypes.object, }; export default ImageLink; diff --git a/src/Components/ImagesTable/ImagesTable.js b/src/Components/ImagesTable/ImagesTable.js index 1fea6081..f28eaa48 100644 --- a/src/Components/ImagesTable/ImagesTable.js +++ b/src/Components/ImagesTable/ImagesTable.js @@ -2,11 +2,28 @@ import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { Link, useNavigate } from 'react-router-dom'; -import { TableComposable, Thead, Tr, Th, Tbody, Td, ActionsColumn, ExpandableRowContent } from '@patternfly/react-table'; -import { EmptyState, EmptyStateVariant, EmptyStateIcon, EmptyStateBody, EmptyStateSecondaryActions, - Pagination, - Toolbar, ToolbarContent, ToolbarItem, - Title } from '@patternfly/react-core'; +import { + TableComposable, + Thead, + Tr, + Th, + Tbody, + Td, + ActionsColumn, + ExpandableRowContent, +} from '@patternfly/react-table'; +import { + EmptyState, + EmptyStateVariant, + EmptyStateIcon, + EmptyStateBody, + EmptyStateSecondaryActions, + Pagination, + Toolbar, + ToolbarContent, + ToolbarItem, + Title, +} from '@patternfly/react-core'; import { PlusCircleIcon } from '@patternfly/react-icons'; import './ImagesTable.scss'; import { composesGet, composeGetStatus } from '../../store/actions/actions'; @@ -18,196 +35,255 @@ import ImageLink from './ImageLink'; import ErrorDetails from './ImageBuildErrorDetails'; const ImagesTable = () => { - const [ page, setPage ] = useState(1); - const [ perPage, setPerPage ] = useState(10); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(10); - const [ expandedComposeIds, setExpandedComposeIds ] = useState([]); - const isExpanded = compose => expandedComposeIds.includes(compose.id); + const [expandedComposeIds, setExpandedComposeIds] = useState([]); + const isExpanded = (compose) => expandedComposeIds.includes(compose.id); - const handleToggle = (compose, isExpanding) => { - if (isExpanding) { - setExpandedComposeIds([ ...expandedComposeIds, compose.id ]); - } else { - setExpandedComposeIds(expandedComposeIds.filter(id => id !== compose.id)); - }}; + const handleToggle = (compose, isExpanding) => { + if (isExpanding) { + setExpandedComposeIds([...expandedComposeIds, compose.id]); + } else { + setExpandedComposeIds( + expandedComposeIds.filter((id) => id !== compose.id) + ); + } + }; - const composes = useSelector((state) => state.composes); - const dispatch = useDispatch(); + const composes = useSelector((state) => state.composes); + const dispatch = useDispatch(); - const navigate = useNavigate(); + const navigate = useNavigate(); - const pollComposeStatuses = () => { - 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; - } + const pollComposeStatuses = () => { + 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; + } - dispatch(composeGetStatus(id)); - }); - }; + dispatch(composeGetStatus(id)); + }); + }; - useEffect(() => { - dispatch(composesGet(perPage, 0)); - const intervalId = setInterval(() => pollComposeStatuses(), 8000); + useEffect(() => { + dispatch(composesGet(perPage, 0)); + const intervalId = setInterval(() => pollComposeStatuses(), 8000); - // clean up interval on unmount - return () => clearInterval(intervalId); - }, []); + // clean up interval on unmount + return () => clearInterval(intervalId); + }, []); - const onSetPage = (_, page) => { - // if the next page's composes haven't been fetched from api yet - // then fetch them with proper page index and offset - if (composes.count > composes.allIds.length) { - const pageIndex = page - 1; - const offset = pageIndex * perPage; - dispatch(composesGet(perPage, offset)); - } + const onSetPage = (_, page) => { + // if the next page's composes haven't been fetched from api yet + // then fetch them with proper page index and offset + if (composes.count > composes.allIds.length) { + const pageIndex = page - 1; + const offset = pageIndex * perPage; + dispatch(composesGet(perPage, offset)); + } - setPage(page); - }; + setPage(page); + }; - const onPerPageSelect = (_, perPage) => { - // if the new per page quantity is greater than the number of already fetched composes fetch more composes - // if all composes haven't already been fetched - if (composes.count > composes.allIds.length && perPage > composes.allIds.length) { - dispatch(composesGet(perPage, 0)); - } + const onPerPageSelect = (_, perPage) => { + // if the new per page quantity is greater than the number of already fetched composes fetch more composes + // if all composes haven't already been fetched + if ( + composes.count > composes.allIds.length && + perPage > composes.allIds.length + ) { + dispatch(composesGet(perPage, 0)); + } - // page should be reset to the first page when the page size is changed. - setPerPage(perPage); - setPage(1); - }; + // page should be reset to the first page when the page size is changed. + setPerPage(perPage); + setPage(1); + }; - const timestampToDisplayString = (ts) => { - // timestamp has format 2021-04-27 12:31:12.794809 +0000 UTC - // must be converted to ms timestamp and then reformatted to Apr 27, 2021 - if (!ts) { - return ''; - } + const timestampToDisplayString = (ts) => { + // timestamp has format 2021-04-27 12:31:12.794809 +0000 UTC + // must be converted to ms timestamp and then reformatted to Apr 27, 2021 + if (!ts) { + return ''; + } - // get YYYY-MM-DD format - const date = ts.slice(0, 10); - const ms = Date.parse(date); - const options = { month: 'short', day: 'numeric', year: 'numeric' }; - const tsDisplay = new Intl.DateTimeFormat('en-US', options).format(ms); - return tsDisplay; - }; + // get YYYY-MM-DD format + const date = ts.slice(0, 10); + const ms = Date.parse(date); + const options = { month: 'short', day: 'numeric', year: 'numeric' }; + const tsDisplay = new Intl.DateTimeFormat('en-US', options).format(ms); + return tsDisplay; + }; - const actions = (compose) => [ - { - title: 'Recreate image', - onClick: () => navigate( - '/imagewizard', - { state: { composeRequest: compose.request, initialStep: 'review' }} - ) - } - ]; + const actions = (compose) => [ + { + title: 'Recreate image', + onClick: () => + navigate('/imagewizard', { + state: { composeRequest: compose.request, initialStep: 'review' }, + }), + }, + ]; - // the state.page is not an index so must be reduced by 1 get the starting index - const itemsStartInclusive = (page - 1) * perPage; - const itemsEndExclusive = itemsStartInclusive + perPage; + // the state.page is not an index so must be reduced by 1 get the starting index + const itemsStartInclusive = (page - 1) * perPage; + const itemsEndExclusive = itemsStartInclusive + perPage; - return ( + return ( + + {(composes.allIds.length === 0 && ( + + + + Create an image + + + 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. + + + Create image + + + + + + )) || ( - { composes.allIds.length === 0 && ( - - - - Create an image - - - 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. - - - Create image - - - - - - ) || ( - - - - - - Create image - - - - - - - - - - - - Image name - Created - Release - Target - Status - Instance - - - - { composes.allIds.slice(itemsStartInclusive, itemsEndExclusive).map((id, rowIndex) => { - const compose = composes.byId[id]; - return ( - - - handleToggle(compose, !isExpanded(compose)) } } /> - {compose.request.image_name || id} - {timestampToDisplayString(compose.created_at)} - - - - - - - - - - UUID -
{ id }
- -
- - - - ); - }) } -
-
- )} + + + + + Create image + + + + + + + + + + + + Image name + Created + Release + Target + Status + Instance + + + + {composes.allIds + .slice(itemsStartInclusive, itemsEndExclusive) + .map((id, rowIndex) => { + const compose = composes.byId[id]; + return ( + + + + handleToggle(compose, !isExpanded(compose)), + }} + /> + + {compose.request.image_name || id} + + + {timestampToDisplayString(compose.created_at)} + + + + + + + + + + + + + + + + + + + + + UUID +
{id}
+ +
+ + + + ); + })} +
- ); + )} +
+ ); }; ImagesTable.propTypes = { - composes: PropTypes.object, - composesGet: PropTypes.func, - composeGetStatus: PropTypes.func, + composes: PropTypes.object, + composesGet: PropTypes.func, + composeGetStatus: PropTypes.func, }; export default ImagesTable; diff --git a/src/Components/ImagesTable/Release.js b/src/Components/ImagesTable/Release.js index 763011b4..686c2452 100644 --- a/src/Components/ImagesTable/Release.js +++ b/src/Components/ImagesTable/Release.js @@ -5,18 +5,20 @@ import { Label } from '@patternfly/react-core'; import { RHEL_8, RHEL_9 } from '../../constants.js'; const Release = (props) => { - const releaseOptions = { - [RHEL_8]: 'RHEL 8', - [RHEL_9]: 'RHEL 9', - 'centos-8': 'CentOS Stream 8', - 'centos-9': 'CentOS Stream 9', - }; - const release = releaseOptions[props.release] ? releaseOptions[props.release] : props.release; - return ; + const releaseOptions = { + [RHEL_8]: 'RHEL 8', + [RHEL_9]: 'RHEL 9', + 'centos-8': 'CentOS Stream 8', + 'centos-9': 'CentOS Stream 9', + }; + const release = releaseOptions[props.release] + ? releaseOptions[props.release] + : props.release; + return ; }; Release.propTypes = { - release: PropTypes.string, + release: PropTypes.string, }; export default Release; diff --git a/src/Components/ImagesTable/Target.js b/src/Components/ImagesTable/Target.js index 4fb2a5ec..eee8f525 100644 --- a/src/Components/ImagesTable/Target.js +++ b/src/Components/ImagesTable/Target.js @@ -2,32 +2,28 @@ import React from 'react'; import PropTypes from 'prop-types'; const Target = (props) => { - const targetOptions = { - aws: 'Amazon Web Services', - azure: 'Microsoft Azure', - gcp: 'Google Cloud Platform', - vsphere: 'VMWare', - 'guest-image': 'Virtualization - Guest image', - 'image-installer': 'Bare metal - Installer' - }; + const targetOptions = { + aws: 'Amazon Web Services', + azure: 'Microsoft Azure', + gcp: 'Google Cloud Platform', + vsphere: 'VMWare', + 'guest-image': 'Virtualization - Guest image', + 'image-installer': 'Bare metal - Installer', + }; - let target; - if (props.uploadType === 'aws.s3') { - target = targetOptions[props.imageType]; - } else { - target = targetOptions[props.uploadType]; - } + let target; + if (props.uploadType === 'aws.s3') { + target = targetOptions[props.imageType]; + } else { + target = targetOptions[props.uploadType]; + } - return ( - <> - {target} - - ); + return <>{target}; }; Target.propTypes = { - uploadType: PropTypes.string, - imageType: PropTypes.string + uploadType: PropTypes.string, + imageType: PropTypes.string, }; export default Target; diff --git a/src/Components/LandingPage/LandingPage.js b/src/Components/LandingPage/LandingPage.js index fd1fd21f..80ff9175 100644 --- a/src/Components/LandingPage/LandingPage.js +++ b/src/Components/LandingPage/LandingPage.js @@ -2,7 +2,11 @@ import React, { Component } from 'react'; -import { PageHeader, PageHeaderTitle } from '@redhat-cloud-services/frontend-components'; +// eslint-disable-next-line rulesdir/disallow-fec-relative-imports +import { + PageHeader, + PageHeaderTitle, +} from '@redhat-cloud-services/frontend-components'; import { Button, Popover, TextContent, Text } from '@patternfly/react-core'; import { GithubIcon, HelpIcon } from '@patternfly/react-icons'; @@ -12,49 +16,57 @@ import './LandingPage.scss'; import DocumentationButton from '../sharedComponents/DocumentationButton'; class LandingPage extends Component { - constructor(props) { - super(props); - } + constructor(props) { + super(props); + } - render() { - return ( - - - - - - Image Builder is a service that allows you to create RHEL images - and push them to cloud environments. - - -
- - }> - -
-
-
- -
-
- ); - } + render() { + return ( + + + + + + Image Builder is a service that allows you to create RHEL + images and push them to cloud environments. + + +
+ + + } + > + +
+
+
+ +
+
+ ); + } } export default LandingPage; diff --git a/src/Components/sharedComponents/DocumentationButton.js b/src/Components/sharedComponents/DocumentationButton.js index a91a3980..90f74b50 100644 --- a/src/Components/sharedComponents/DocumentationButton.js +++ b/src/Components/sharedComponents/DocumentationButton.js @@ -3,19 +3,22 @@ import { Button } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; const DocumentationButton = () => { - const documentationURL = -'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/uploading_a_customized_rhel_system_image_to_cloud_environments/index'; + const documentationURL = + 'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/uploading_a_customized_rhel_system_image_to_cloud_environments/index'; - return ( - - ); + return ( + + ); }; export default DocumentationButton; diff --git a/src/Router.js b/src/Router.js index 0b399510..9786f408 100644 --- a/src/Router.js +++ b/src/Router.js @@ -2,13 +2,15 @@ import React, { lazy } from 'react'; import { Route, Routes } from 'react-router-dom'; const LandingPage = lazy(() => import('./Components/LandingPage/LandingPage')); -const CreateImageWizard = lazy(() => import('./Components/CreateImageWizard/CreateImageWizard')); +const CreateImageWizard = lazy(() => + import('./Components/CreateImageWizard/CreateImageWizard') +); export const Router = () => { - return ( - - } /> - } /> - - ); + return ( + + } /> + } /> + + ); }; diff --git a/src/Utilities/getBaseName.js b/src/Utilities/getBaseName.js index 59b7ff38..4ed3d83c 100644 --- a/src/Utilities/getBaseName.js +++ b/src/Utilities/getBaseName.js @@ -1,15 +1,15 @@ function getBaseName(pathname) { - let release = '/'; - const pathName = pathname.split('/'); + let release = '/'; + const pathName = pathname.split('/'); + pathName.shift(); + + if (pathName[0] === 'beta') { pathName.shift(); + release = `/beta/`; + } - if (pathName[0] === 'beta') { - pathName.shift(); - release = `/beta/`; - } - - return `${release}${pathName[0]}/${pathName[1] || ''}`; + return `${release}${pathName[0]}/${pathName[1] || ''}`; } export default getBaseName; diff --git a/src/Utilities/isRhel.js b/src/Utilities/isRhel.js index 4ea6163f..6e582073 100644 --- a/src/Utilities/isRhel.js +++ b/src/Utilities/isRhel.js @@ -1,16 +1,13 @@ -import { - RHEL_8, - RHEL_9, -} from '../constants'; +import { RHEL_8, RHEL_9 } from '../constants'; function isRhel(distro) { - switch (distro) { - case RHEL_8: - case RHEL_9: - return true; - default: - return false; - } + switch (distro) { + case RHEL_8: + case RHEL_9: + return true; + default: + return false; + } } export default isRhel; diff --git a/src/api.js b/src/api.js index efd3d7a2..3cf645e4 100644 --- a/src/api.js +++ b/src/api.js @@ -1,62 +1,63 @@ import axios from 'axios'; -import { - IMAGE_BUILDER_API, - RHSM_API, -} from './constants'; +import { IMAGE_BUILDER_API, RHSM_API } from './constants'; -const postHeaders = { headers: { 'Content-Type': 'application/json' }}; +const postHeaders = { headers: { 'Content-Type': 'application/json' } }; async function composeImage(body) { - let path = '/compose'; - const request = await axios.post(IMAGE_BUILDER_API.concat(path), body, postHeaders); - return request.data; + let path = '/compose'; + const request = await axios.post( + IMAGE_BUILDER_API.concat(path), + body, + postHeaders + ); + return request.data; } async function getComposes(limit, offset) { - const params = new URLSearchParams({ - limit, - offset, - }); - let path = '/composes?' + params.toString(); - const request = await axios.get(IMAGE_BUILDER_API.concat(path)); - return request.data; + const params = new URLSearchParams({ + limit, + offset, + }); + let path = '/composes?' + params.toString(); + const request = await axios.get(IMAGE_BUILDER_API.concat(path)); + return request.data; } async function getComposeStatus(id) { - let path = '/composes/' + id; - const request = await axios.get(IMAGE_BUILDER_API.concat(path)); - return request.data; + let path = '/composes/' + id; + const request = await axios.get(IMAGE_BUILDER_API.concat(path)); + return request.data; } async function getPackages(distribution, architecture, search, limit) { - const params = new URLSearchParams({ - distribution, - architecture, - search, - }); - limit && params.append('limit', limit); - let path = '/packages?' + params.toString(); - const request = await axios.get(IMAGE_BUILDER_API.concat(path)); - return request.data; + const params = new URLSearchParams({ + distribution, + architecture, + search, + }); + limit && params.append('limit', limit); + let path = '/packages?' + params.toString(); + const request = await axios.get(IMAGE_BUILDER_API.concat(path)); + return request.data; } async function getVersion() { - let path = '/version'; - const request = await axios.get(IMAGE_BUILDER_API.concat(path)); - return request.data; + let path = '/version'; + const request = await axios.get(IMAGE_BUILDER_API.concat(path)); + return request.data; } async function getActivationKeys() { - const path = '/activation_keys'; - const request = await axios.get(RHSM_API.concat(path)); - return request.data.body; + const path = '/activation_keys'; + const request = await axios.get(RHSM_API.concat(path)); + return request.data.body; } export default { - composeImage, - getComposes, - getComposeStatus, - getPackages, - getVersion, - getActivationKeys, + composeImage, + getComposes, + getComposeStatus, + getPackages, + getVersion, + getActivationKeys, }; diff --git a/src/constants.js b/src/constants.js index 991219de..af5a05dc 100644 --- a/src/constants.js +++ b/src/constants.js @@ -8,8 +8,8 @@ export const UNIT_MIB = 1024 ** 2; export const UNIT_GIB = 1024 ** 3; export const RELEASES = { - [RHEL_8]: 'Red Hat Enterprise Linux (RHEL) 8', - [RHEL_9]: 'Red Hat Enterprise Linux (RHEL) 9', - 'centos-8': 'CentOS Stream 8', - 'centos-9': 'CentOS Stream 9', + [RHEL_8]: 'Red Hat Enterprise Linux (RHEL) 8', + [RHEL_9]: 'Red Hat Enterprise Linux (RHEL) 9', + 'centos-8': 'CentOS Stream 8', + 'centos-9': 'CentOS Stream 9', }; diff --git a/src/entry.test.js b/src/entry.test.js index 18b3a987..77307f99 100644 --- a/src/entry.test.js +++ b/src/entry.test.js @@ -1,13 +1,15 @@ import getBaseName from './Utilities/getBaseName'; describe('Utilities/getBaseName', () => { - it('should find the right base name on Stable ', () => { - expect(getBaseName('/insights/foo/bar/baz')).toEqual('/insights/foo'); - expect(getBaseName('/rhcs/bar/bar/baz')).toEqual('/rhcs/bar'); - }); + it('should find the right base name on Stable ', () => { + expect(getBaseName('/insights/foo/bar/baz')).toEqual('/insights/foo'); + expect(getBaseName('/rhcs/bar/bar/baz')).toEqual('/rhcs/bar'); + }); - it('should find the right base name on Beta ', () => { - expect(getBaseName('/beta/insights/foo/bar/baz')).toEqual('/beta/insights/foo'); - expect(getBaseName('/beta/test/fff/bar/baz')).toEqual('/beta/test/fff'); - }); + it('should find the right base name on Beta ', () => { + expect(getBaseName('/beta/insights/foo/bar/baz')).toEqual( + '/beta/insights/foo' + ); + expect(getBaseName('/beta/test/fff/bar/baz')).toEqual('/beta/test/fff'); + }); }); diff --git a/src/store/actions/actions.js b/src/store/actions/actions.js index c7cc5142..37d1ab10 100644 --- a/src/store/actions/actions.js +++ b/src/store/actions/actions.js @@ -2,150 +2,157 @@ import api from '../../api'; import types from '../types'; function composeUpdated(compose) { - return { - type: types.COMPOSE_UPDATED, - payload: { compose }, - }; + return { + type: types.COMPOSE_UPDATED, + payload: { compose }, + }; } export const composeFailed = (error) => ({ - type: types.COMPOSE_FAILED, - payload: { error } + type: types.COMPOSE_FAILED, + payload: { error }, }); export const composeAdded = (compose, insert) => ({ - type: types.COMPOSE_ADDED, - payload: { compose, insert }, + type: types.COMPOSE_ADDED, + payload: { compose, insert }, }); -export const composeStart = (composeRequest) => async dispatch => { - // response will be of the format {id: ''} - const request = api.composeImage(composeRequest); - return request.then(response => { - // add the compose id to the compose object to provide access to the id if iterating through - // composes and add an image status of 'pending' alongside the compose request. - const compose = Object.assign({}, response, { request: composeRequest }, { image_status: { status: 'pending' }}); - dispatch(composeAdded(compose, true)); - }).catch(err => { - if (err.response.status === 500) { - dispatch(composeFailed('Error: Something went wrong serverside')); - } else { - dispatch(composeFailed('Error: Something went wrong with the compose')); - } +export const composeStart = (composeRequest) => async (dispatch) => { + // response will be of the format {id: ''} + const request = api.composeImage(composeRequest); + return request + .then((response) => { + // add the compose id to the compose object to provide access to the id if iterating through + // composes and add an image status of 'pending' alongside the compose request. + const compose = Object.assign( + {}, + response, + { request: composeRequest }, + { image_status: { status: 'pending' } } + ); + dispatch(composeAdded(compose, true)); + }) + .catch((err) => { + if (err.response.status === 500) { + dispatch(composeFailed('Error: Something went wrong serverside')); + } else { + dispatch(composeFailed('Error: Something went wrong with the compose')); + } }); }; export const composeUpdatedStatus = (id, status) => ({ - type: types.COMPOSE_UPDATED_STATUS, - payload: { id, status } + type: types.COMPOSE_UPDATED_STATUS, + payload: { id, status }, }); -export const composeGetStatus = (id) => async dispatch => { - const request = await api.getComposeStatus(id); - dispatch(composeUpdatedStatus(id, request.image_status)); +export const composeGetStatus = (id) => async (dispatch) => { + const request = await api.getComposeStatus(id); + dispatch(composeUpdatedStatus(id, request.image_status)); }; export const composesUpdatedCount = (count) => ({ - type: types.COMPOSES_UPDATED_COUNT, - payload: { count } + type: types.COMPOSES_UPDATED_COUNT, + payload: { count }, }); -export const composesGet = (limit, offset) => async dispatch => { - const request = await api.getComposes(limit, offset); - request.data.map(compose => { - dispatch(composeAdded(compose, false)); - dispatch(composeGetStatus(compose.id)); - }); - dispatch(composesUpdatedCount(request.meta.count)); +export const composesGet = (limit, offset) => async (dispatch) => { + const request = await api.getComposes(limit, offset); + request.data.map((compose) => { + dispatch(composeAdded(compose, false)); + dispatch(composeGetStatus(compose.id)); + }); + dispatch(composesUpdatedCount(request.meta.count)); }; function setRelease({ arch, distro }) { - return { - type: types.SET_RELEASE, - payload: { - arch, - distro, - } - }; + return { + type: types.SET_RELEASE, + payload: { + arch, + distro, + }, + }; } function setUploadDestinations({ aws, azure, google }) { - return { - type: types.SET_UPLOAD_DESTINATIONS, - payload: { - aws, - azure, - google, - } - }; + return { + type: types.SET_UPLOAD_DESTINATIONS, + payload: { + aws, + azure, + google, + }, + }; } function setUploadAWS({ shareWithAccounts }) { - return { - type: types.SET_UPLOAD_AWS, - payload: { - shareWithAccounts, - } - }; + return { + type: types.SET_UPLOAD_AWS, + payload: { + shareWithAccounts, + }, + }; } function setUploadAzure({ tenantId, subscriptionId, resourceGroup }) { - return { - type: types.SET_UPLOAD_AZURE, - payload: { - tenantId, - subscriptionId, - resourceGroup, - } - }; + return { + type: types.SET_UPLOAD_AZURE, + payload: { + tenantId, + subscriptionId, + resourceGroup, + }, + }; } function setUploadGoogle({ accountType, shareWithAccounts }) { - return { - type: types.SET_UPLOAD_GOOGLE, - payload: { - accountType, - shareWithAccounts, - } - }; + return { + type: types.SET_UPLOAD_GOOGLE, + payload: { + accountType, + shareWithAccounts, + }, + }; } function setSelectedPackages(selectedPackages) { - return { - type: types.SET_SELECTED_PACKAGES, - payload: selectedPackages - }; + return { + type: types.SET_SELECTED_PACKAGES, + payload: selectedPackages, + }; } function setSubscription({ activationKey, insights, organization }) { - return { - type: types.SET_SUBSCRIPTION, - payload: { - activationKey, - insights, - organization, - } - }; + return { + type: types.SET_SUBSCRIPTION, + payload: { + activationKey, + insights, + organization, + }, + }; } function setSubscribeNow(subscribeNow) { - return { - type: types.SET_SUBSCRIBE_NOW, - payload: subscribeNow - }; + return { + type: types.SET_SUBSCRIBE_NOW, + payload: subscribeNow, + }; } export default { - composesGet, - composeStart, - composeUpdated, - composeGetStatus, - setRelease, - setUploadDestinations, - setUploadAWS, - setUploadAzure, - setUploadGoogle, - setSelectedPackages, - setSubscription, - setSubscribeNow, + composesGet, + composeStart, + composeUpdated, + composeGetStatus, + setRelease, + setUploadDestinations, + setUploadAWS, + setUploadAzure, + setUploadGoogle, + setSelectedPackages, + setSubscription, + setSubscribeNow, }; diff --git a/src/store/index.js b/src/store/index.js index af434d4f..e4eff819 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -7,32 +7,32 @@ import composes from './reducers/composes'; let registry; -export function init (store = {}, ...middleware) { - if (!registry) { - registry = new ReducerRegistry(store, [ - promiseMiddleware, - thunk, - ...middleware.filter(item => typeof item !== 'undefined'), - ]); +export function init(store = {}, ...middleware) { + if (!registry) { + registry = new ReducerRegistry(store, [ + promiseMiddleware, + thunk, + ...middleware.filter((item) => typeof item !== 'undefined'), + ]); - registry.register({ - composes, - notifications: notificationsReducer, - }); - } + registry.register({ + composes, + notifications: notificationsReducer, + }); + } - return registry; + return registry; } -export function getStore () { - return registry.getStore(); +export function getStore() { + return registry.getStore(); } -export function register (...args) { - return registry.register(...args); +export function register(...args) { + return registry.register(...args); } /* added for testing purposes only */ export function clearStore() { - registry = undefined; + registry = undefined; } diff --git a/src/store/reducers/composes.js b/src/store/reducers/composes.js index 938abb04..121f3a5e 100644 --- a/src/store/reducers/composes.js +++ b/src/store/reducers/composes.js @@ -25,74 +25,78 @@ import types from '../types'; // }; const initialComposesState = { - count: 0, - allIds: [], - byId: {}, - error: null, + count: 0, + allIds: [], + byId: {}, + error: null, }; // only add to array if compose does not exist const updateAllIds = (allIds, id, insert) => { - if (allIds.includes(id)) { - return allIds; - } + if (allIds.includes(id)) { + return allIds; + } - if (insert) { - return [ id ].concat(allIds); - } + if (insert) { + return [id].concat(allIds); + } - return allIds.concat(id); + return allIds.concat(id); }; export function composes(state = initialComposesState, action) { - switch (action.type) { - case types.COMPOSE_ADDED: - return { - ...state, - allIds: updateAllIds(state.allIds, action.payload.compose.id, action.payload.insert), - byId: { - ...state.byId, - [action.payload.compose.id]: action.payload.compose, - }, - error: null, - }; - case types.COMPOSE_FAILED: - return { - ...state, - error: action.payload.error, - }; - case types.COMPOSE_PENDING: - return { - ...state, - error: null, - }; - case types.COMPOSE_UPDATED: - return { - ...state, - byId: { - ...state.byId, - [action.payload.compose.id]: action.payload.compose, - } - }; - case types.COMPOSES_UPDATED_COUNT: - return { - ...state, - count: action.payload.count, - }; - case types.COMPOSE_UPDATED_STATUS: - return { - ...state, - byId: { - ...state.byId, - [action.payload.id]: { - ...state.byId[action.payload.id], - image_status: action.payload.status, - } - } - }; - default: - return state; - } + switch (action.type) { + case types.COMPOSE_ADDED: + return { + ...state, + allIds: updateAllIds( + state.allIds, + action.payload.compose.id, + action.payload.insert + ), + byId: { + ...state.byId, + [action.payload.compose.id]: action.payload.compose, + }, + error: null, + }; + case types.COMPOSE_FAILED: + return { + ...state, + error: action.payload.error, + }; + case types.COMPOSE_PENDING: + return { + ...state, + error: null, + }; + case types.COMPOSE_UPDATED: + return { + ...state, + byId: { + ...state.byId, + [action.payload.compose.id]: action.payload.compose, + }, + }; + case types.COMPOSES_UPDATED_COUNT: + return { + ...state, + count: action.payload.count, + }; + case types.COMPOSE_UPDATED_STATUS: + return { + ...state, + byId: { + ...state.byId, + [action.payload.id]: { + ...state.byId[action.payload.id], + image_status: action.payload.status, + }, + }, + }; + default: + return state; + } } export default composes; diff --git a/src/store/types.js b/src/store/types.js index 6972aaad..d969ca9b 100644 --- a/src/store/types.js +++ b/src/store/types.js @@ -13,17 +13,17 @@ const SET_SUBSCRIPTION = 'SET_SUBSCRIPTION'; const SET_SUBSCRIBE_NOW = 'SET_SUBSCRIBE_NOW'; export default { - COMPOSE_ADDED, - COMPOSE_FAILED, - COMPOSE_UPDATED, - COMPOSES_UPDATED_COUNT, - COMPOSE_UPDATED_STATUS, - SET_RELEASE, - SET_UPLOAD_DESTINATIONS, - SET_UPLOAD_AWS, - SET_UPLOAD_AZURE, - SET_UPLOAD_GOOGLE, - SET_SELECTED_PACKAGES, - SET_SUBSCRIPTION, - SET_SUBSCRIBE_NOW, + COMPOSE_ADDED, + COMPOSE_FAILED, + COMPOSE_UPDATED, + COMPOSES_UPDATED_COUNT, + COMPOSE_UPDATED_STATUS, + SET_RELEASE, + SET_UPLOAD_DESTINATIONS, + SET_UPLOAD_AWS, + SET_UPLOAD_AZURE, + SET_UPLOAD_GOOGLE, + SET_SELECTED_PACKAGES, + SET_SUBSCRIPTION, + SET_SUBSCRIBE_NOW, }; diff --git a/src/test/Components/CreateImageWizard/CreateImageWizard.test.js b/src/test/Components/CreateImageWizard/CreateImageWizard.test.js index 3deb4e20..197cd72d 100644 --- a/src/test/Components/CreateImageWizard/CreateImageWizard.test.js +++ b/src/test/Components/CreateImageWizard/CreateImageWizard.test.js @@ -1,7 +1,13 @@ import '@testing-library/jest-dom'; import React from 'react'; -import { screen, waitFor, waitForElementToBeRemoved, within, act } from '@testing-library/react'; +import { + screen, + waitFor, + waitForElementToBeRemoved, + within, + act, +} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWithReduxRouter } from '../../testUtils'; import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard'; @@ -12,1671 +18,1812 @@ let history = undefined; let store = undefined; function verifyButtons() { - // these buttons exist everywhere - const next = screen.getByRole('button', { name: /Next/ }); - const back = screen.getByRole('button', { name: /Back/ }); - const cancel = screen.getByRole('button', { name: /Cancel/ }); + // these buttons exist everywhere + const next = screen.getByRole('button', { name: /Next/ }); + const back = screen.getByRole('button', { name: /Back/ }); + const cancel = screen.getByRole('button', { name: /Cancel/ }); - return [ next, back, cancel ]; + return [next, back, cancel]; } function verifyCancelButton(cancel, history) { - cancel.click(); + cancel.click(); - expect(history.location.pathname).toBe('/'); + expect(history.location.pathname).toBe('/'); } // packages const mockPkgResult = { - meta: { count: 3 }, - links: { first: '', last: '' }, - data: [ - { - name: 'testPkg', - summary: 'test package summary', - version: '1.0', - }, - { - name: 'lib-test', - summary: 'lib-test package summary', - version: '1.0', - }, - { - name: 'test', - summary: 'summary for test package', - version: '1.0', - } - ] + meta: { count: 3 }, + links: { first: '', last: '' }, + data: [ + { + name: 'testPkg', + summary: 'test package summary', + version: '1.0', + }, + { + name: 'lib-test', + summary: 'lib-test package summary', + version: '1.0', + }, + { + name: 'test', + summary: 'summary for test package', + version: '1.0', + }, + ], }; const mockPkgResultAlpha = { - meta: { count: 3 }, - links: { first: '', last: '' }, - data: [ - { - name: 'lib-test', - summary: 'lib-test package summary', - version: '1.0', - }, - { - name: 'Z-test', - summary: 'Z-test package summary', - version: '1.0', - }, - { - name: 'test', - summary: 'summary for test package', - version: '1.0', - } - ] + meta: { count: 3 }, + links: { first: '', last: '' }, + data: [ + { + name: 'lib-test', + summary: 'lib-test package summary', + version: '1.0', + }, + { + name: 'Z-test', + summary: 'Z-test package summary', + version: '1.0', + }, + { + name: 'test', + summary: 'summary for test package', + version: '1.0', + }, + ], }; const mockPkgResultPartial = { - meta: { count: 132 }, - links: { first: '', last: '' }, - data: new Array(100).fill().map((_, i) => { - return { name: 'testPkg-' + i, summary: 'test package summary', version: '1.0' }; - }) + meta: { count: 132 }, + links: { first: '', last: '' }, + data: new Array(100).fill().map((_, i) => { + return { + name: 'testPkg-' + i, + summary: 'test package summary', + version: '1.0', + }; + }), }; const mockPkgResultAll = { - meta: { count: 132 }, - links: { first: '', last: '' }, - data: new Array(132).fill().map((_, i) => { - return { name: 'testPkg-' + i, summary: 'test package summary', version: '1.0' }; - }) + meta: { count: 132 }, + links: { first: '', last: '' }, + data: new Array(132).fill().map((_, i) => { + return { + name: 'testPkg-' + i, + summary: 'test package summary', + version: '1.0', + }; + }), }; const mockPkgResultEmpty = { - meta: { count: 0 }, - links: { first: '', last: '' }, - data: null + meta: { count: 0 }, + links: { first: '', last: '' }, + data: null, }; const searchForAvailablePackages = async (searchbox, searchTerm) => { - userEvent.type(searchbox, searchTerm); - await act(async() => { - screen.getByRole('button', { name: /search button for available packages/i }).click(); - }); + userEvent.type(searchbox, searchTerm); + await act(async () => { + screen + .getByRole('button', { name: /search button for available packages/i }) + .click(); + }); }; const searchForChosenPackages = async (searchbox, searchTerm) => { - if (!searchTerm) { - userEvent.clear(searchbox); - } else { - userEvent.type(searchbox, searchTerm); - } + if (!searchTerm) { + userEvent.clear(searchbox); + } else { + userEvent.type(searchbox, searchTerm); + } }; // mock the insights dependency beforeAll(() => { - // scrollTo is not defined in jsdom - window.HTMLElement.prototype.scrollTo = function() {}; + // scrollTo is not defined in jsdom + window.HTMLElement.prototype.scrollTo = function () {}; - // mock the activation key api call - const mockActivationKeys = [{ name: 'name0' }, { name: 'name1' }]; - jest - .spyOn(api, 'getActivationKeys') - .mockImplementation(() => Promise.resolve(mockActivationKeys)); + // mock the activation key api call + const mockActivationKeys = [{ name: 'name0' }, { name: 'name1' }]; + jest + .spyOn(api, 'getActivationKeys') + .mockImplementation(() => Promise.resolve(mockActivationKeys)); - global.insights = { - chrome: { - auth: { - getUser: () => { - return { - identity: { - internal: { - org_id: 5 - } - } - }; - } + global.insights = { + chrome: { + auth: { + getUser: () => { + return { + identity: { + internal: { + org_id: 5, + }, }, - isBeta: () => { - return true; - }, - } - }; + }; + }, + }, + isBeta: () => { + return true; + }, + }, + }; }); afterEach(() => { - jest.clearAllMocks(); - history = undefined; + jest.clearAllMocks(); + history = undefined; }); // restore global mock afterAll(() => { - global.insights = undefined; + global.insights = undefined; }); describe('Create Image Wizard', () => { - test('renders component', () => { - renderWithReduxRouter(); - // check heading - screen.getByRole('heading', { name: /Create image/ }); + test('renders component', () => { + renderWithReduxRouter(); + // check heading + screen.getByRole('heading', { name: /Create image/ }); - screen.getByRole('button', { name: 'Image output' }); - screen.getByRole('button', { name: 'Packages' }); - screen.getByRole('button', { name: 'Name image' }); - screen.getByRole('button', { name: 'Review' }); - }); + screen.getByRole('button', { name: 'Image output' }); + screen.getByRole('button', { name: 'Packages' }); + screen.getByRole('button', { name: 'Name image' }); + screen.getByRole('button', { name: 'Review' }); + }); }); describe('Step Image output', () => { - const setUp = () => { - history = renderWithReduxRouter().history; + const setUp = () => { + history = renderWithReduxRouter().history; - const imageOutputLink = screen.getByRole('button', { name: 'Image output' }); - - // select aws as upload destination - const awsTile = screen.getByTestId('upload-aws'); - awsTile.click(); - - // load from sidebar - imageOutputLink.click(); - }; - - test('clicking Next loads Upload to AWS', () => { - setUp(); - - const [ next, , ] = verifyButtons(); - next.click(); - - screen.getByText('AWS account ID'); + const imageOutputLink = screen.getByRole('button', { + name: 'Image output', }); - test('clicking Cancel loads landing page', () => { - setUp(); + // select aws as upload destination + const awsTile = screen.getByTestId('upload-aws'); + awsTile.click(); - const [ , , cancel ] = verifyButtons(); - verifyCancelButton(cancel, history); + // load from sidebar + imageOutputLink.click(); + }; + + test('clicking Next loads Upload to AWS', () => { + setUp(); + + const [next, ,] = verifyButtons(); + next.click(); + + screen.getByText('AWS account ID'); + }); + + test('clicking Cancel loads landing page', () => { + setUp(); + + const [, , cancel] = verifyButtons(); + verifyCancelButton(cancel, history); + }); + + test('target environment is required', () => { + setUp(); + + const destination = screen.getByTestId('target-select'); + const required = within(destination).getByText('*'); + expect(destination).toBeEnabled(); + expect(destination).toContainElement(required); + }); + + test('selecting and deselecting a tile disables the next button', () => { + setUp(); + + const [next, ,] = verifyButtons(); + + const awsTile = screen.getByTestId('upload-aws'); + // this has already been clicked once in the setup function + awsTile.click(); // deselect + + const googleTile = screen.getByTestId('upload-google'); + googleTile.click(); // select + googleTile.click(); // deselect + + const azureTile = screen.getByTestId('upload-azure'); + azureTile.click(); // select + azureTile.click(); // deselect + + expect(next).toBeDisabled(); + }); + + test('expected releases are present on beta', async () => { + setUp(); + + const releaseMenu = screen.getByRole('button', { + name: /options menu/i, + }); + userEvent.click(releaseMenu); + + await screen.findByRole('option', { + name: 'Red Hat Enterprise Linux (RHEL) 8', + }); + await screen.findByRole('option', { + name: 'Red Hat Enterprise Linux (RHEL) 9', + }); + await screen.findByRole('option', { + name: 'CentOS Stream 8', + }); + await screen.findByRole('option', { + name: 'CentOS Stream 9', }); - test('target environment is required', () => { - setUp(); - - const destination = screen.getByTestId('target-select'); - const required = within(destination).getByText('*'); - expect(destination).toBeEnabled(); - expect(destination).toContainElement(required); - }); - - test('selecting and deselecting a tile disables the next button', () => { - setUp(); - - const [ next, , ] = verifyButtons(); - - const awsTile = screen.getByTestId('upload-aws'); - // this has already been clicked once in the setup function - awsTile.click(); // deselect - - const googleTile = screen.getByTestId('upload-google'); - googleTile.click(); // select - googleTile.click(); // deselect - - const azureTile = screen.getByTestId('upload-azure'); - azureTile.click(); // select - azureTile.click(); // deselect - - expect(next).toBeDisabled(); - }); - - test('expected releases are present on beta', async () => { - setUp(); - - const releaseMenu = screen.getByRole('button', { - name: /options menu/i - }); - userEvent.click(releaseMenu); - - await screen.findByRole('option', { - name: 'Red Hat Enterprise Linux (RHEL) 8' - }); - await screen.findByRole('option', { - name: 'Red Hat Enterprise Linux (RHEL) 9' - }); - await screen.findByRole('option', { - name: 'CentOS Stream 8' - }); - await screen.findByRole('option', { - name: 'CentOS Stream 9' - }); - - userEvent.click(releaseMenu); - }); + userEvent.click(releaseMenu); + }); }); describe('Step Upload to AWS', () => { - const setUp = () => { - history = renderWithReduxRouter().history; - const [ next, , ] = verifyButtons(); + const setUp = () => { + history = renderWithReduxRouter().history; + const [next, ,] = verifyButtons(); - // select aws as upload destination - const awsTile = screen.getByTestId('upload-aws'); - awsTile.click(); + // select aws as upload destination + const awsTile = screen.getByTestId('upload-aws'); + awsTile.click(); - next.click(); + next.click(); - expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Target environment - Amazon Web Service'); - }; + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent( + 'Target environment - Amazon Web Service' + ); + }; - test('clicking Next loads Registration', async () => { - setUp(); + test('clicking Next loads Registration', async () => { + setUp(); - userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); - const [ next, , ] = verifyButtons(); - next.click(); + userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); + const [next, ,] = verifyButtons(); + next.click(); - await screen.findByRole('textbox', { - name: 'Select activation key' - }); - - screen.getByText('Register images with Red Hat'); + await screen.findByRole('textbox', { + name: 'Select activation key', }); - test('clicking Back loads Release', () => { - setUp(); + screen.getByText('Register images with Red Hat'); + }); - const [ , back, ] = verifyButtons(); - back.click(); + test('clicking Back loads Release', () => { + setUp(); - screen.getByTestId('upload-aws'); - }); + const [, back] = verifyButtons(); + back.click(); - test('clicking Cancel loads landing page', () => { - setUp(); + screen.getByTestId('upload-aws'); + }); - const [ , , cancel ] = verifyButtons(); - verifyCancelButton(cancel, history); - }); + test('clicking Cancel loads landing page', () => { + setUp(); - test('the aws account id fieldis shown and required', () => { - setUp(); + const [, , cancel] = verifyButtons(); + verifyCancelButton(cancel, history); + }); - const accessKeyId = screen.getByTestId('aws-account-id'); - expect(accessKeyId).toHaveValue(''); - expect(accessKeyId).toBeEnabled(); - // expect(accessKeyId).toBeRequired(); // DDf does not support required value - }); + test('the aws account id fieldis shown and required', () => { + setUp(); + + const accessKeyId = screen.getByTestId('aws-account-id'); + expect(accessKeyId).toHaveValue(''); + expect(accessKeyId).toBeEnabled(); + // expect(accessKeyId).toBeRequired(); // DDf does not support required value + }); }); describe('Step Upload to Google', () => { - const setUp = () => { - history = renderWithReduxRouter().history; - const [ next, , ] = verifyButtons(); + const setUp = () => { + history = renderWithReduxRouter().history; + const [next, ,] = verifyButtons(); - // select aws as upload destination - const awsTile = screen.getByTestId('upload-google'); - awsTile.click(); + // select aws as upload destination + const awsTile = screen.getByTestId('upload-google'); + awsTile.click(); - next.click(); + next.click(); - expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Target environment - Google Cloud Platform'); - }; + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent( + 'Target environment - Google Cloud Platform' + ); + }; - test('clicking Next loads Registration', async () => { - setUp(); + test('clicking Next loads Registration', async () => { + setUp(); - userEvent.type(screen.getByTestId('input-google-email'), 'test@test.com'); - const [ next, , ] = verifyButtons(); - next.click(); + userEvent.type(screen.getByTestId('input-google-email'), 'test@test.com'); + const [next, ,] = verifyButtons(); + next.click(); - await screen.findByRole('textbox', { - name: 'Select activation key' - }); - - screen.getByText('Register images with Red Hat'); + await screen.findByRole('textbox', { + name: 'Select activation key', }); - test('clicking Back loads Release', () => { - setUp(); + screen.getByText('Register images with Red Hat'); + }); - const [ , back, ] = verifyButtons(); - back.click(); + test('clicking Back loads Release', () => { + setUp(); - screen.getByTestId('upload-google'); - }); + const [, back] = verifyButtons(); + back.click(); - test('clicking Cancel loads landing page', () => { - setUp(); + screen.getByTestId('upload-google'); + }); - const [ , , cancel ] = verifyButtons(); - verifyCancelButton(cancel, history); - }); + test('clicking Cancel loads landing page', () => { + setUp(); - test('the google account id field is shown and required', () => { - setUp(); + const [, , cancel] = verifyButtons(); + verifyCancelButton(cancel, history); + }); - const accessKeyId = screen.getByTestId('input-google-email'); - expect(accessKeyId).toHaveValue(''); - expect(accessKeyId).toBeEnabled(); - // expect(accessKeyId).toBeRequired(); // DDf does not support required value - }); + test('the google account id field is shown and required', () => { + setUp(); - test('the google email field must be a valid email', () => { - setUp(); + const accessKeyId = screen.getByTestId('input-google-email'); + expect(accessKeyId).toHaveValue(''); + expect(accessKeyId).toBeEnabled(); + // expect(accessKeyId).toBeRequired(); // DDf does not support required value + }); - const [ next, , ] = verifyButtons(); - userEvent.type(screen.getByTestId('input-google-email'), 'a'); - expect(next).toHaveClass('pf-m-disabled'); - expect(next).toBeDisabled(); - userEvent.type(screen.getByTestId('input-google-email'), 'test@test.com'); - expect(next).not.toHaveClass('pf-m-disabled'); - expect(next).toBeEnabled(); - }); + test('the google email field must be a valid email', () => { + setUp(); + + const [next, ,] = verifyButtons(); + userEvent.type(screen.getByTestId('input-google-email'), 'a'); + expect(next).toHaveClass('pf-m-disabled'); + expect(next).toBeDisabled(); + userEvent.type(screen.getByTestId('input-google-email'), 'test@test.com'); + expect(next).not.toHaveClass('pf-m-disabled'); + expect(next).toBeEnabled(); + }); }); describe('Step Upload to Azure', () => { - const setUp = () => { - history = renderWithReduxRouter().history; - const [ next, , ] = verifyButtons(); + const setUp = () => { + history = renderWithReduxRouter().history; + const [next, ,] = verifyButtons(); - // select aws as upload destination - const awsTile = screen.getByTestId('upload-azure'); - awsTile.click(); - next.click(); + // select aws as upload destination + const awsTile = screen.getByTestId('upload-azure'); + awsTile.click(); + next.click(); - expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Target environment - Microsoft Azure'); - }; + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent( + 'Target environment - Microsoft Azure' + ); + }; - test('clicking Next loads Registration', async () => { - setUp(); - // Randomly generated GUID - userEvent.type(screen.getByTestId('azure-tenant-id'), 'b8f86d22-4371-46ce-95e7-65c415f3b1e2'); - userEvent.type(screen.getByTestId('azure-subscription-id'), '60631143-a7dc-4d15-988b-ba83f3c99711'); - userEvent.type(screen.getByTestId('azure-resource-group'), 'testResourceGroup'); + test('clicking Next loads Registration', async () => { + setUp(); + // Randomly generated GUID + userEvent.type( + screen.getByTestId('azure-tenant-id'), + 'b8f86d22-4371-46ce-95e7-65c415f3b1e2' + ); + userEvent.type( + screen.getByTestId('azure-subscription-id'), + '60631143-a7dc-4d15-988b-ba83f3c99711' + ); + userEvent.type( + screen.getByTestId('azure-resource-group'), + 'testResourceGroup' + ); - const [ next, , ] = verifyButtons(); - next.click(); + const [next, ,] = verifyButtons(); + next.click(); - await screen.findByRole('textbox', { - name: 'Select activation key' - }); - - screen.getByText('Register images with Red Hat'); + await screen.findByRole('textbox', { + name: 'Select activation key', }); - test('clicking Back loads Release', () => { - setUp(); + screen.getByText('Register images with Red Hat'); + }); - const [ , back, ] = verifyButtons(); - back.click(); + test('clicking Back loads Release', () => { + setUp(); - screen.getByTestId('upload-azure'); - }); + const [, back] = verifyButtons(); + back.click(); - test('clicking Cancel loads landing page', () => { - setUp(); + screen.getByTestId('upload-azure'); + }); - const [ , , cancel ] = verifyButtons(); - verifyCancelButton(cancel, history); - }); + test('clicking Cancel loads landing page', () => { + setUp(); - test('the azure upload fields are shown and required', () => { - setUp(); + const [, , cancel] = verifyButtons(); + verifyCancelButton(cancel, history); + }); - const tenantId = screen.getByTestId('azure-tenant-id'); - expect(tenantId).toHaveValue(''); - expect(tenantId).toBeEnabled(); - // expect(tenantId).toBeRequired(); // DDf does not support required value + test('the azure upload fields are shown and required', () => { + setUp(); - const subscription = screen.getByTestId('azure-subscription-id'); - expect(subscription).toHaveValue(''); - expect(subscription).toBeEnabled(); - // expect(subscription).toBeRequired(); // DDf does not support required value + const tenantId = screen.getByTestId('azure-tenant-id'); + expect(tenantId).toHaveValue(''); + expect(tenantId).toBeEnabled(); + // expect(tenantId).toBeRequired(); // DDf does not support required value - const resourceGroup = screen.getByTestId('azure-resource-group'); - expect(resourceGroup).toHaveValue(''); - expect(resourceGroup).toBeEnabled(); - // expect(resourceGroup).toBeRequired(); // DDf does not support required value - }); + const subscription = screen.getByTestId('azure-subscription-id'); + expect(subscription).toHaveValue(''); + expect(subscription).toBeEnabled(); + // expect(subscription).toBeRequired(); // DDf does not support required value + + const resourceGroup = screen.getByTestId('azure-resource-group'); + expect(resourceGroup).toHaveValue(''); + expect(resourceGroup).toBeEnabled(); + // expect(resourceGroup).toBeRequired(); // DDf does not support required value + }); }); describe('Step Registration', () => { - const setUp = async () => { - history = renderWithReduxRouter().history; - const [ next, , ] = verifyButtons(); + const setUp = async () => { + history = renderWithReduxRouter().history; + const [next, ,] = verifyButtons(); - // select aws as upload destination - const awsTile = screen.getByTestId('upload-aws'); - awsTile.click(); + // select aws as upload destination + const awsTile = screen.getByTestId('upload-aws'); + awsTile.click(); - next.click(); - userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); + next.click(); + userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); - next.click(); + next.click(); - await screen.findByRole('textbox', { - name: 'Select activation key' - }); - }; - - test('clicking Next loads file system configuration', async () => { - await setUp(); - - const registerLaterRadio = screen.getByLabelText('Register later'); - userEvent.click(registerLaterRadio); - - const [ next, , ] = verifyButtons(); - next.click(); - - screen.getByTestId('fsc-paritioning-toggle'); + await screen.findByRole('textbox', { + name: 'Select activation key', }); + }; - test('clicking Back loads Upload to AWS', async () => { - await setUp(); + test('clicking Next loads file system configuration', async () => { + await setUp(); - const [ , back, ] = verifyButtons(); - back.click(); + const registerLaterRadio = screen.getByLabelText('Register later'); + userEvent.click(registerLaterRadio); - screen.getByText('AWS account ID'); + const [next, ,] = verifyButtons(); + next.click(); + + screen.getByTestId('fsc-paritioning-toggle'); + }); + + test('clicking Back loads Upload to AWS', async () => { + await setUp(); + + const [, back] = verifyButtons(); + back.click(); + + screen.getByText('AWS account ID'); + }); + + test('clicking Cancel loads landing page', async () => { + await setUp(); + + const [, , cancel] = verifyButtons(); + verifyCancelButton(cancel, history); + }); + + test('should allow registering with insights', async () => { + await setUp(); + + const registrationRadio = screen.getByLabelText( + 'Register and connect image instances with Red Hat' + ); + userEvent.click(registrationRadio); + + const activationKeyDropdown = await screen.findByRole('textbox', { + name: 'Select activation key', }); - - test('clicking Cancel loads landing page', async () => { - await setUp(); - - const [ , , cancel ] = verifyButtons(); - verifyCancelButton(cancel, history); + userEvent.click(activationKeyDropdown); + const activationKey = await screen.findByRole('option', { + name: 'name0', }); + userEvent.click(activationKey); + screen.getByDisplayValue('name0'); - test('should allow registering with insights', async () => { - await setUp(); - - const registrationRadio = screen.getByLabelText('Register and connect image instances with Red Hat'); - userEvent.click(registrationRadio); - - const activationKeyDropdown = await screen.findByRole('textbox', { - name: 'Select activation key' - }); - userEvent.click(activationKeyDropdown); - const activationKey = await screen.findByRole('option', { - name: 'name0' - }); - userEvent.click(activationKey); - screen.getByDisplayValue('name0'); - - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - await waitFor(() => { - screen.getByText('Register with Subscriptions and Red Hat Insights'); - screen.getAllByText('012345678901'); - }); + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + await waitFor(() => { + screen.getByText('Register with Subscriptions and Red Hat Insights'); + screen.getAllByText('012345678901'); }); + }); - test('should allow registering without insights', async () => { - await setUp(); + test('should allow registering without insights', async () => { + await setUp(); - const registrationRadio = screen.getByLabelText('Register image instances only'); - userEvent.click(registrationRadio); + const registrationRadio = screen.getByLabelText( + 'Register image instances only' + ); + userEvent.click(registrationRadio); - const activationKeyDropdown = await screen.findByRole('textbox', { - name: 'Select activation key' - }); - userEvent.click(activationKeyDropdown); - const activationKey = await screen.findByRole('option', { - name: 'name0' - }); - userEvent.click(activationKey); - screen.getByDisplayValue('name0'); - - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - await waitFor(() => { - screen.getByText('Register with Subscriptions'); - screen.getAllByText('012345678901'); - }); + const activationKeyDropdown = await screen.findByRole('textbox', { + name: 'Select activation key', }); - - test('should hide input fields when clicking Register the system later', async () => { - await setUp(); - // first check the other radio button which causes extra widgets to be shown - const registrationRadio = screen.getByLabelText('Register and connect image instances with Red Hat'); - userEvent.click(registrationRadio); - - const p1 = waitForElementToBeRemoved(() => [ - screen.getByTestId('subscription-activation-key'), - ]); - - // then click the later radio button which should remove any input fields - const registerLaterRadio = screen.getByLabelText('Register later'); - userEvent.click(registerLaterRadio); - - await p1; - - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByText('Register the system later'); + userEvent.click(activationKeyDropdown); + const activationKey = await screen.findByRole('option', { + name: 'name0', }); + userEvent.click(activationKey); + screen.getByDisplayValue('name0'); + + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + await waitFor(() => { + screen.getByText('Register with Subscriptions'); + screen.getAllByText('012345678901'); + }); + }); + + test('should hide input fields when clicking Register the system later', async () => { + await setUp(); + // first check the other radio button which causes extra widgets to be shown + const registrationRadio = screen.getByLabelText( + 'Register and connect image instances with Red Hat' + ); + userEvent.click(registrationRadio); + + const p1 = waitForElementToBeRemoved(() => [ + screen.getByTestId('subscription-activation-key'), + ]); + + // then click the later radio button which should remove any input fields + const registerLaterRadio = screen.getByLabelText('Register later'); + userEvent.click(registerLaterRadio); + + await p1; + + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByText('Register the system later'); + }); }); describe('Step Packages', () => { - const setUp = async () => { - history = renderWithReduxRouter().history; - const [ next, , ] = verifyButtons(); + const setUp = async () => { + history = renderWithReduxRouter().history; + const [next, ,] = verifyButtons(); - // select aws as upload destination - const awsTile = screen.getByTestId('upload-aws'); - awsTile.click(); - next.click(); + // select aws as upload destination + const awsTile = screen.getByTestId('upload-aws'); + awsTile.click(); + next.click(); - // aws step - userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); - next.click(); - // skip registration - await screen.findByRole('textbox', { - name: 'Select activation key' - }); - - const registerLaterRadio = screen.getByLabelText('Register later'); - userEvent.click(registerLaterRadio); - next.click(); - - // skip fsc - next.click(); - }; - - test('clicking Next loads Image name', async () => { - await setUp(); - - const [ next, , ] = verifyButtons(); - next.click(); - - screen.getByRole('heading', { - name: 'Name image' - }); + // aws step + userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); + next.click(); + // skip registration + await screen.findByRole('textbox', { + name: 'Select activation key', }); - test('clicking Back loads file system configuration', async () => { - await setUp(); + const registerLaterRadio = screen.getByLabelText('Register later'); + userEvent.click(registerLaterRadio); + next.click(); - const back = screen.getByRole('button', { name: /Back/ }); - back.click(); + // skip fsc + next.click(); + }; - screen.getByTestId('fsc-paritioning-toggle'); + test('clicking Next loads Image name', async () => { + await setUp(); + + const [next, ,] = verifyButtons(); + next.click(); + + screen.getByRole('heading', { + name: 'Name image', }); + }); - test('clicking Cancel loads landing page', async () => { - await setUp(); + test('clicking Back loads file system configuration', async () => { + await setUp(); - const [ , , cancel ] = verifyButtons(); - verifyCancelButton(cancel, history); + const back = screen.getByRole('button', { name: /Back/ }); + back.click(); + + screen.getByTestId('fsc-paritioning-toggle'); + }); + + test('clicking Cancel loads landing page', async () => { + await setUp(); + + const [, , cancel] = verifyButtons(); + verifyCancelButton(cancel, history); + }); + + test('should display search bar and button', async () => { + await setUp(); + + userEvent.type(screen.getByTestId('search-available-pkgs-input'), 'test'); + + screen.getByRole('button', { + name: 'Search button for available packages', }); + }); - test('should display search bar and button', async () => { - await setUp(); + test('should display default state', async () => { + await setUp(); - userEvent.type(screen.getByTestId('search-available-pkgs-input'), 'test'); + screen.getByText('Search above to add additionalpackages to your image'); + screen.getByText('No packages added'); + }); - screen.getByRole('button', { - name: 'Search button for available packages' - }); + test('search results should be sorted with most relevant results first', async () => { + await setUp(); + + const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + + searchbox.click(); + + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); + + await searchForAvailablePackages(searchbox, 'test'); + expect(getPackages).toHaveBeenCalledTimes(1); + + const availablePackagesList = screen.getByTestId('available-pkgs-list'); + const availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + expect(availablePackagesItems).toHaveLength(3); + const [firstItem, secondItem, thirdItem] = availablePackagesItems; + expect(firstItem).toHaveTextContent('testsummary for test package'); + expect(secondItem).toHaveTextContent('testPkgtest package summary'); + expect(thirdItem).toHaveTextContent('lib-testlib-test package summary'); + }); + + test('search results should be sorted after selecting them and then deselecting them', async () => { + await setUp(); + + const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + + searchbox.click(); + + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); + + await searchForAvailablePackages(searchbox, 'test'); + expect(getPackages).toHaveBeenCalledTimes(1); + + screen + .getByRole('option', { name: /testPkg test package summary/ }) + .click(); + screen.getByRole('button', { name: /Add selected/ }).click(); + + screen + .getByRole('option', { name: /testPkg test package summary/ }) + .click(); + screen.getByRole('button', { name: /Remove selected/ }).click(); + + const availablePackagesList = screen.getByTestId('available-pkgs-list'); + const availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + expect(availablePackagesItems).toHaveLength(3); + const [firstItem, secondItem, thirdItem] = availablePackagesItems; + expect(firstItem).toHaveTextContent('testsummary for test package'); + expect(secondItem).toHaveTextContent('testPkgtest package summary'); + expect(thirdItem).toHaveTextContent('lib-testlib-test package summary'); + }); + + test('search results should be sorted after adding and then removing all packages', async () => { + await setUp(); + + const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + + searchbox.click(); + + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); + + await searchForAvailablePackages(searchbox, 'test'); + expect(getPackages).toHaveBeenCalledTimes(1); + + screen.getByRole('button', { name: /Add all/ }).click(); + screen.getByRole('button', { name: /Remove all/ }).click(); + + const availablePackagesList = screen.getByTestId('available-pkgs-list'); + const availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + expect(availablePackagesItems).toHaveLength(3); + const [firstItem, secondItem, thirdItem] = availablePackagesItems; + expect(firstItem).toHaveTextContent('testsummary for test package'); + expect(secondItem).toHaveTextContent('testPkgtest package summary'); + expect(thirdItem).toHaveTextContent('lib-testlib-test package summary'); + }); + + test('removing a single package updates the state correctly', async () => { + await setUp(); + + const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + searchbox.click(); + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); + + await searchForAvailablePackages(searchbox, 'test'); + expect(getPackages).toHaveBeenCalledTimes(1); + screen.getByRole('button', { name: /Add all/ }).click(); + + // remove a single package + screen + .getByRole('option', { name: /lib-test lib-test package summary/ }) + .click(); + screen.getByRole('button', { name: /Remove selected/ }).click(); + + // skip name page + screen.getByRole('button', { name: /Next/ }).click(); + + // review page + screen.getByRole('button', { name: /Next/ }).click(); + + // await screen.findByTestId('chosen-packages-count'); + let chosen = await screen.findByTestId('chosen-packages-count'); + expect(chosen).toHaveTextContent('2'); + + // remove another package + screen.getByRole('button', { name: /Back/ }).click(); + screen.getByRole('button', { name: /Back/ }).click(); + await screen.findByTestId('search-available-pkgs-input'); + screen.getByRole('option', { name: /summary for test package/ }).click(); + screen.getByRole('button', { name: /Remove selected/ }).click(); + + // review page + screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + + // await screen.findByTestId('chosen-packages-count'); + chosen = await screen.findByTestId('chosen-packages-count'); + expect(chosen).toHaveTextContent('1'); + }); + + test('should display empty available state on failed search', async () => { + await setUp(); + + const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + + searchbox.click(); + + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResultEmpty)); + + await searchForAvailablePackages(searchbox, 'asdf'); + expect(getPackages).toHaveBeenCalledTimes(1); + screen.getByText('No packages found'); + }); + + test('should display empty available state on failed search after a successful search', async () => { + await setUp(); + + const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + + searchbox.click(); + + let getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); + + await searchForAvailablePackages(searchbox, 'test'); + + getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResultEmpty)); + + await searchForAvailablePackages(searchbox, 'asdf'); + + expect(getPackages).toHaveBeenCalledTimes(2); + screen.getByText('No packages found'); + }); + + test('should display empty chosen state on failed search', async () => { + await setUp(); + + const searchboxAvailable = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + const searchboxChosen = screen.getAllByRole('textbox')[1]; + + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); + + searchboxAvailable.click(); + await searchForAvailablePackages(searchboxAvailable, 'test'); + expect(getPackages).toHaveBeenCalledTimes(1); + + screen.getByRole('button', { name: /Add all/ }).click(); + + searchboxChosen.click(); + userEvent.type(searchboxChosen, 'asdf'); + + expect(screen.getAllByText('No packages found').length === 2); + // We need to clear this input in order to not have sideeffects on other tests + await searchForChosenPackages(searchboxChosen, ''); + }); + + test('should filter chosen packages from available list', async () => { + await setUp(); + + const searchboxAvailable = screen.getAllByRole('textbox')[0]; + const availablePackagesList = screen.getByTestId('available-pkgs-list'); + const chosenPackagesList = screen.getByTestId('chosen-pkgs-list'); + + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); + + searchboxAvailable.click(); + await searchForAvailablePackages(searchboxAvailable, 'test'); + expect(getPackages).toHaveBeenCalledTimes(1); + + let availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + expect(availablePackagesItems).toHaveLength(3); + + screen + .getByRole('option', { name: /testPkg test package summary/ }) + .click(); + screen.getByRole('button', { name: /Add selected/ }).click(); + + availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + expect(availablePackagesItems).toHaveLength(2); + + let chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); + // Knowing if it is in document isn't enough. We want a specific length of 1 so ignore rule. + // eslint-disable-next-line jest-dom/prefer-in-document + expect(chosenPackagesItems).toHaveLength(1); + + searchboxAvailable.click(); + await searchForAvailablePackages(searchboxAvailable, 'test'); + expect(getPackages).toHaveBeenCalledTimes(2); + + availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); + expect(availablePackagesItems).toHaveLength(2); + // Knowing if it is in document isn't enough. We want a specific length of 1 so ignore rule. + // eslint-disable-next-line jest-dom/prefer-in-document + expect(chosenPackagesItems).toHaveLength(1); + within(chosenPackagesList).getByRole('option', { + name: /testPkg test package summary/, }); + }); - test('should display default state', async () => { - await setUp(); + test('should get all packages, regardless of api default limit', async () => { + await setUp(); - screen.getByText('Search above to add additionalpackages to your image'); - screen.getByText('No packages added'); - }); + const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref - test('search results should be sorted with most relevant results first', async () => { - await setUp(); + searchbox.click(); - const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation((distribution, architecture, search, limit) => { + return limit + ? Promise.resolve(mockPkgResultAll) + : Promise.resolve(mockPkgResultPartial); + }); - searchbox.click(); + await searchForAvailablePackages(searchbox, 'testPkg'); + expect(getPackages).toHaveBeenCalledTimes(2); - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); + const availablePackagesList = screen.getByTestId('available-pkgs-list'); + const availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + expect(availablePackagesItems).toHaveLength(132); + }); - await searchForAvailablePackages(searchbox, 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); + test('search results should be sorted alphabetically', async () => { + await setUp(); - const availablePackagesList = screen.getByTestId('available-pkgs-list'); - const availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(3); - const [ firstItem, secondItem, thirdItem ] = availablePackagesItems; - expect(firstItem).toHaveTextContent('testsummary for test package'); - expect(secondItem).toHaveTextContent('testPkgtest package summary'); - expect(thirdItem).toHaveTextContent('lib-testlib-test package summary'); - }); + const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref - test('search results should be sorted after selecting them and then deselecting them', async () => { - await setUp(); + searchbox.click(); - const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResultAlpha)); - searchbox.click(); + await searchForAvailablePackages(searchbox, 'test'); + expect(getPackages).toHaveBeenCalledTimes(1); - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); + const availablePackagesList = screen.getByTestId('available-pkgs-list'); + const availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + expect(availablePackagesItems).toHaveLength(3); - await searchForAvailablePackages(searchbox, 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); + const [firstItem, secondItem, thirdItem] = availablePackagesItems; + expect(firstItem).toHaveTextContent('testsummary for test package'); + expect(secondItem).toHaveTextContent('lib-testlib-test package summary'); + expect(thirdItem).toHaveTextContent('Z-testZ-test package summary'); + }); - screen.getByRole('option', { name: /testPkg test package summary/ }).click(); - screen.getByRole('button', { name: /Add selected/ }).click(); + test('available packages can be reset', async () => { + await setUp(); - screen.getByRole('option', { name: /testPkg test package summary/ }).click(); - screen.getByRole('button', { name: /Remove selected/ }).click(); + const searchbox = screen.getAllByRole('textbox')[0]; - const availablePackagesList = screen.getByTestId('available-pkgs-list'); - const availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(3); - const [ firstItem, secondItem, thirdItem ] = availablePackagesItems; - expect(firstItem).toHaveTextContent('testsummary for test package'); - expect(secondItem).toHaveTextContent('testPkgtest package summary'); - expect(thirdItem).toHaveTextContent('lib-testlib-test package summary'); - }); + searchbox.click(); - test('search results should be sorted after adding and then removing all packages', async () => { - await setUp(); + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); - const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref + await searchForAvailablePackages(searchbox, 'test'); + expect(getPackages).toHaveBeenCalledTimes(1); - searchbox.click(); + const availablePackagesList = screen.getByTestId('available-pkgs-list'); + const availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + expect(availablePackagesItems).toHaveLength(3); - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); + screen + .getByRole('button', { name: /clear available packages search/i }) + .click(); - await searchForAvailablePackages(searchbox, 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); + screen.getByText('Search above to add additionalpackages to your image'); + }); - screen.getByRole('button', { name: /Add all/ }).click(); - screen.getByRole('button', { name: /Remove all/ }).click(); + test('chosen packages can be reset after filtering', async () => { + await setUp(); - const availablePackagesList = screen.getByTestId('available-pkgs-list'); - const availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(3); - const [ firstItem, secondItem, thirdItem ] = availablePackagesItems; - expect(firstItem).toHaveTextContent('testsummary for test package'); - expect(secondItem).toHaveTextContent('testPkgtest package summary'); - expect(thirdItem).toHaveTextContent('lib-testlib-test package summary'); - }); + const availableSearchbox = screen.getAllByRole('textbox')[0]; - test('removing a single package updates the state correctly', async () => { - await setUp(); + availableSearchbox.click(); - const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref - searchbox.click(); - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); - await searchForAvailablePackages(searchbox, 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); - screen.getByRole('button', { name: /Add all/ }).click(); + await searchForAvailablePackages(availableSearchbox, 'test'); + expect(getPackages).toHaveBeenCalledTimes(1); - // remove a single package - screen.getByRole('option', { name: /lib-test lib-test package summary/ }).click(); - screen.getByRole('button', { name: /Remove selected/ }).click(); + const availablePackagesList = screen.getByTestId('available-pkgs-list'); + const availablePackagesItems = within(availablePackagesList).getAllByRole( + 'option' + ); + expect(availablePackagesItems).toHaveLength(3); - // skip name page - screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Add all/ }).click(); - // review page - screen.getByRole('button', { name: /Next/ }).click(); + const chosenPackagesList = screen.getByTestId('chosen-pkgs-list'); + let chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); + expect(chosenPackagesItems).toHaveLength(3); - // await screen.findByTestId('chosen-packages-count'); - let chosen = await screen.findByTestId('chosen-packages-count'); - expect(chosen).toHaveTextContent('2'); + const chosenSearchbox = screen.getAllByRole('textbox')[1]; + chosenSearchbox.click(); + await searchForChosenPackages(chosenSearchbox, 'Pkg'); + chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); + // eslint-disable-next-line jest-dom/prefer-in-document + expect(chosenPackagesItems).toHaveLength(1); - // remove another package - screen.getByRole('button', { name: /Back/ }).click(); - screen.getByRole('button', { name: /Back/ }).click(); - await screen.findByTestId('search-available-pkgs-input'); - screen.getByRole('option', { name: /summary for test package/ }).click(); - screen.getByRole('button', { name: /Remove selected/ }).click(); - - // review page - screen.getByRole('button', { name: /Next/ }).click(); - screen.getByRole('button', { name: /Next/ }).click(); - - // await screen.findByTestId('chosen-packages-count'); - chosen = await screen.findByTestId('chosen-packages-count'); - expect(chosen).toHaveTextContent('1'); - }); - - test('should display empty available state on failed search', async () => { - await setUp(); - - const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref - - searchbox.click(); - - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResultEmpty)); - - await searchForAvailablePackages(searchbox, 'asdf'); - expect(getPackages).toHaveBeenCalledTimes(1); - screen.getByText('No packages found'); - }); - - test('should display empty available state on failed search after a successful search', async () => { - await setUp(); - - const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref - - searchbox.click(); - - let getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); - - await searchForAvailablePackages(searchbox, 'test'); - - getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResultEmpty)); - - await searchForAvailablePackages(searchbox, 'asdf'); - - expect(getPackages).toHaveBeenCalledTimes(2); - screen.getByText('No packages found'); - }); - - test('should display empty chosen state on failed search', async () => { - await setUp(); - - const searchboxAvailable = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref - const searchboxChosen = screen.getAllByRole('textbox')[1]; - - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); - - searchboxAvailable.click(); - await searchForAvailablePackages(searchboxAvailable, 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); - - screen.getByRole('button', { name: /Add all/ }).click(); - - searchboxChosen.click(); - userEvent.type(searchboxChosen, 'asdf'); - - expect(screen.getAllByText('No packages found').length === 2); - // We need to clear this input in order to not have sideeffects on other tests - await searchForChosenPackages(searchboxChosen, ''); - }); - - test('should filter chosen packages from available list', async () => { - await setUp(); - - const searchboxAvailable = screen.getAllByRole('textbox')[0]; - const availablePackagesList = screen.getByTestId('available-pkgs-list'); - const chosenPackagesList = screen.getByTestId('chosen-pkgs-list'); - - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); - - searchboxAvailable.click(); - await searchForAvailablePackages(searchboxAvailable, 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); - - let availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(3); - - screen.getByRole('option', { name: /testPkg test package summary/ }).click(); - screen.getByRole('button', { name: /Add selected/ }).click(); - - availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(2); - - let chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); - // Knowing if it is in document isn't enough. We want a specific length of 1 so ignore rule. - // eslint-disable-next-line jest-dom/prefer-in-document - expect(chosenPackagesItems).toHaveLength(1); - - searchboxAvailable.click(); - await searchForAvailablePackages(searchboxAvailable, 'test'); - expect(getPackages).toHaveBeenCalledTimes(2); - - availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(2); - // Knowing if it is in document isn't enough. We want a specific length of 1 so ignore rule. - // eslint-disable-next-line jest-dom/prefer-in-document - expect(chosenPackagesItems).toHaveLength(1); - within(chosenPackagesList).getByRole('option', { name: /testPkg test package summary/ }); - }); - - test('should get all packages, regardless of api default limit', async () => { - await setUp(); - - const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref - - searchbox.click(); - - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation((distribution, architecture, search, limit) => { - return limit ? Promise.resolve(mockPkgResultAll) : Promise.resolve(mockPkgResultPartial); - }); - - await searchForAvailablePackages(searchbox, 'testPkg'); - expect(getPackages).toHaveBeenCalledTimes(2); - - const availablePackagesList = screen.getByTestId('available-pkgs-list'); - const availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(132); - }); - - test('search results should be sorted alphabetically', async () => { - await setUp(); - - const searchbox = screen.getAllByRole('textbox')[0]; // searching by id doesn't update the input ref - - searchbox.click(); - - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResultAlpha)); - - await searchForAvailablePackages(searchbox, 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); - - const availablePackagesList = screen.getByTestId('available-pkgs-list'); - const availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(3); - - const [ firstItem, secondItem, thirdItem ] = availablePackagesItems; - expect(firstItem).toHaveTextContent('testsummary for test package'); - expect(secondItem).toHaveTextContent('lib-testlib-test package summary'); - expect(thirdItem).toHaveTextContent('Z-testZ-test package summary'); - }); - - test('available packages can be reset', async () => { - await setUp(); - - const searchbox = screen.getAllByRole('textbox')[0]; - - searchbox.click(); - - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); - - await searchForAvailablePackages(searchbox, 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); - - const availablePackagesList = screen.getByTestId('available-pkgs-list'); - const availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(3); - - screen.getByRole('button', { name: /clear available packages search/i }).click(); - - screen.getByText('Search above to add additionalpackages to your image'); - }); - - test('chosen packages can be reset after filtering', async () => { - await setUp(); - - const availableSearchbox = screen.getAllByRole('textbox')[0]; - - availableSearchbox.click(); - - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); - - await searchForAvailablePackages(availableSearchbox, 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); - - const availablePackagesList = screen.getByTestId('available-pkgs-list'); - const availablePackagesItems = within(availablePackagesList).getAllByRole('option'); - expect(availablePackagesItems).toHaveLength(3); - - screen.getByRole('button', { name: /Add all/ }).click(); - - const chosenPackagesList = screen.getByTestId('chosen-pkgs-list'); - let chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); - expect(chosenPackagesItems).toHaveLength(3); - - const chosenSearchbox = screen.getAllByRole('textbox')[1]; - chosenSearchbox.click(); - await searchForChosenPackages(chosenSearchbox, 'Pkg'); - chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); - // eslint-disable-next-line jest-dom/prefer-in-document - expect(chosenPackagesItems).toHaveLength(1); - - screen.getByRole('button', { name: /clear chosen packages search/i }).click(); - chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); - expect(chosenPackagesItems).toHaveLength(3); - }); + screen + .getByRole('button', { name: /clear chosen packages search/i }) + .click(); + chosenPackagesItems = within(chosenPackagesList).getAllByRole('option'); + expect(chosenPackagesItems).toHaveLength(3); + }); }); describe('Step Details', () => { - const setUp = async () => { - history = renderWithReduxRouter().history; - const [ next, , ] = verifyButtons(); + const setUp = async () => { + history = renderWithReduxRouter().history; + const [next, ,] = verifyButtons(); - // select aws as upload destination - const awsTile = screen.getByTestId('upload-aws'); - awsTile.click(); - next.click(); + // select aws as upload destination + const awsTile = screen.getByTestId('upload-aws'); + awsTile.click(); + next.click(); - // aws step - userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); - next.click(); - // skip registration - await screen.findByRole('textbox', { - name: 'Select activation key' - }); - - const registerLaterRadio = screen.getByLabelText('Register later'); - userEvent.click(registerLaterRadio); - next.click(); - - // skip fsc - next.click(); - // skip packages - next.click(); - }; - - test('image name invalid for more than 100 chars', async () => { - await setUp(); - - const [ next, , ] = verifyButtons(); - - // Enter image name - const nameInput = screen.getByRole('textbox', { - name: 'Image name' - }); - // 101 character name - const invalidName = 'a'.repeat(101); - userEvent.type(nameInput, invalidName); - expect(next).toHaveClass('pf-m-disabled'); - expect(next).toBeDisabled(); - userEvent.clear(nameInput); - userEvent.type(nameInput, 'validName'); - expect(next).not.toHaveClass('pf-m-disabled'); - expect(next).toBeEnabled(); + // aws step + userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); + next.click(); + // skip registration + await screen.findByRole('textbox', { + name: 'Select activation key', }); + + const registerLaterRadio = screen.getByLabelText('Register later'); + userEvent.click(registerLaterRadio); + next.click(); + + // skip fsc + next.click(); + // skip packages + next.click(); + }; + + test('image name invalid for more than 100 chars', async () => { + await setUp(); + + const [next, ,] = verifyButtons(); + + // Enter image name + const nameInput = screen.getByRole('textbox', { + name: 'Image name', + }); + // 101 character name + const invalidName = 'a'.repeat(101); + userEvent.type(nameInput, invalidName); + expect(next).toHaveClass('pf-m-disabled'); + expect(next).toBeDisabled(); + userEvent.clear(nameInput); + userEvent.type(nameInput, 'validName'); + expect(next).not.toHaveClass('pf-m-disabled'); + expect(next).toBeEnabled(); + }); }); describe('Step Review', () => { - const setUp = async () => { - history = renderWithReduxRouter().history; - const [ next, , ] = verifyButtons(); + const setUp = async () => { + history = renderWithReduxRouter().history; + const [next, ,] = verifyButtons(); - // select aws as upload destination - const awsTile = screen.getByTestId('upload-aws'); - awsTile.click(); - next.click(); + // select aws as upload destination + const awsTile = screen.getByTestId('upload-aws'); + awsTile.click(); + next.click(); - // aws step - userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); - next.click(); + // aws step + userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); + next.click(); - await screen.findByRole('textbox', { - name: 'Select activation key' - }); + await screen.findByRole('textbox', { + name: 'Select activation key', + }); - // skip registration - const registerLaterRadio = screen.getByLabelText('Register later'); - userEvent.click(registerLaterRadio); - next.click(); + // skip registration + const registerLaterRadio = screen.getByLabelText('Register later'); + userEvent.click(registerLaterRadio); + next.click(); - // skip fsc - next.click(); + // skip fsc + next.click(); - // skip packages - next.click(); - // skip name - next.click(); + // skip packages + next.click(); + // skip name + next.click(); + }; + + // eslint-disable-next-line no-unused-vars + const setUpCentOS = () => { + history = renderWithReduxRouter().history; + + const [next, ,] = verifyButtons(); + + const releaseMenu = screen.getByRole('button', { + name: /options menu/i, + }); + userEvent.click(releaseMenu); + const centos = screen.getByRole('option', { + name: 'CentOS Stream 8', + }); + userEvent.click(centos); + + // select aws as upload destination + const awsTile = screen.getByTestId('upload-aws'); + awsTile.click(); + next.click(); + + // aws step + userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); + next.click(); + + // skip fsc + next.click(); + + // skip packages + next.click(); + // skip name + next.click(); + }; + + test('has 3 buttons', async () => { + await setUp(); + + screen.getByRole('button', { name: /Create/ }); + screen.getByRole('button', { name: /Back/ }); + screen.getByRole('button', { name: /Cancel/ }); + }); + + test('clicking Back loads Image name', async () => { + await setUp(); + + const back = screen.getByRole('button', { name: /Back/ }); + back.click(); + + screen.getByRole('heading', { + name: 'Name image', + }); + }); + + test('clicking Cancel loads landing page', async () => { + await setUp(); + + const cancel = screen.getByRole('button', { name: /Cancel/ }); + verifyCancelButton(cancel, history); + }); + + test('has three tabs for rhel', async () => { + await setUp(); + + const buttonTarget = screen.getByTestId('tab-target'); + const buttonRegistration = screen.getByTestId('tab-registration'); + const buttonSystem = screen.getByTestId('tab-system'); + + userEvent.click(buttonTarget); + screen.getByRole('heading', { + name: 'Amazon Web Services', + }); + userEvent.click(buttonRegistration); + screen.getByText('Register the system later'); + userEvent.click(buttonSystem); + screen.getByRole('heading', { + name: 'Packages', + }); + screen.getByRole('heading', { + name: 'File system configuration', + }); + }); + + test('has two tabs for centos', () => { + setUpCentOS(); + + const buttonTarget = screen.getByTestId('tab-target'); + const buttonSystem = screen.getByTestId('tab-system'); + expect(screen.queryByTestId('tab-registration')).not.toBeInTheDocument(); + + userEvent.click(buttonTarget); + screen.getByRole('heading', { + name: 'Amazon Web Services', + }); + userEvent.click(buttonSystem); + screen.getByRole('heading', { + name: 'Packages', + }); + screen.getByRole('heading', { + name: 'File system configuration', + }); + }); + + test('can pass location to recreate on review step', () => { + const initialLocation = { + state: { + composeRequest: { + distribution: RHEL_8, + image_name: 'MyImageName', + image_requests: [ + { + architecture: 'x86_64', + image_type: 'guest-image', + upload_request: { + type: 'aws.s3', + options: {}, + }, + }, + ], + customizations: {}, + }, + initialStep: 'review', + }, }; - - // eslint-disable-next-line no-unused-vars - const setUpCentOS = () => { - history = renderWithReduxRouter().history; - - const [ next, , ] = verifyButtons(); - - const releaseMenu = screen.getByRole('button', { - name: /options menu/i - }); - userEvent.click(releaseMenu); - const centos = screen.getByRole('option', { - name: 'CentOS Stream 8' - }); - userEvent.click(centos); - - // select aws as upload destination - const awsTile = screen.getByTestId('upload-aws'); - awsTile.click(); - next.click(); - - // aws step - userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); - next.click(); - - // skip fsc - next.click(); - - // skip packages - next.click(); - // skip name - next.click(); - }; - - test('has 3 buttons', async () => { - await setUp(); - - screen.getByRole('button', { name: /Create/ }); - screen.getByRole('button', { name: /Back/ }); - screen.getByRole('button', { name: /Cancel/ }); - }); - - test('clicking Back loads Image name', async () => { - await setUp(); - - const back = screen.getByRole('button', { name: /Back/ }); - back.click(); - - screen.getByRole('heading', { - name: 'Name image' - }); - }); - - test('clicking Cancel loads landing page', async () => { - await setUp(); - - const cancel = screen.getByRole('button', { name: /Cancel/ }); - verifyCancelButton(cancel, history); - }); - - test('has three tabs for rhel', async () => { - await setUp(); - - const buttonTarget = screen.getByTestId('tab-target'); - const buttonRegistration = screen.getByTestId('tab-registration'); - const buttonSystem = screen.getByTestId('tab-system'); - - userEvent.click(buttonTarget); - screen.getByRole('heading', { - name: 'Amazon Web Services' - }); - userEvent.click(buttonRegistration); - screen.getByText('Register the system later'); - userEvent.click(buttonSystem); - screen.getByRole('heading', { - name: 'Packages' - }); - screen.getByRole('heading', { - name: 'File system configuration' - }); - }); - - test('has two tabs for centos', () => { - setUpCentOS(); - - const buttonTarget = screen.getByTestId('tab-target'); - const buttonSystem = screen.getByTestId('tab-system'); - expect(screen.queryByTestId('tab-registration')).not.toBeInTheDocument(); - - userEvent.click(buttonTarget); - screen.getByRole('heading', { - name: 'Amazon Web Services' - }); - userEvent.click(buttonSystem); - screen.getByRole('heading', { - name: 'Packages' - }); - screen.getByRole('heading', { - name: 'File system configuration' - }); - }); - - test('can pass location to recreate on review step', () => { - const initialLocation = { - state: { - composeRequest: { - distribution: RHEL_8, - image_name: 'MyImageName', - image_requests: [{ - architecture: 'x86_64', - image_type: 'guest-image', - upload_request: { - type: 'aws.s3', - options: {} - }, - }], - customizations: {} - }, - initialStep: 'review' - } - }; - history = renderWithReduxRouter(, {}, initialLocation).history; - screen.getByText('Review the information and click "Create image" to create the image using the following criteria.'); - screen.getByText('Virtualization - Guest image'); - screen.getByText('Register the system later'); - screen.getByText('MyImageName'); - - }); + history = renderWithReduxRouter( + , + {}, + initialLocation + ).history; + screen.getByText( + 'Review the information and click "Create image" to create the image using the following criteria.' + ); + screen.getByText('Virtualization - Guest image'); + screen.getByText('Register the system later'); + screen.getByText('MyImageName'); + }); }); describe('Click through all steps', () => { - const setUp = async () => { - const view = renderWithReduxRouter(); - history = view.history; - store = view.reduxStore; - }; + const setUp = async () => { + const view = renderWithReduxRouter(); + history = view.history; + store = view.reduxStore; + }; - test('with valid values', async () => { - await setUp(); + test('with valid values', async () => { + await setUp(); - const next = screen.getByRole('button', { name: /Next/ }); + const next = screen.getByRole('button', { name: /Next/ }); - // select image output - const releaseMenu = screen.getByRole('button', { - name: /options menu/i - }); - userEvent.click(releaseMenu); - const releaseOption = screen.getByRole('option', { - name: 'Red Hat Enterprise Linux (RHEL) 8' - }); - userEvent.click(releaseOption); + // select image output + const releaseMenu = screen.getByRole('button', { + name: /options menu/i, + }); + userEvent.click(releaseMenu); + const releaseOption = screen.getByRole('option', { + name: 'Red Hat Enterprise Linux (RHEL) 8', + }); + userEvent.click(releaseOption); - userEvent.click(screen.getByTestId('upload-aws')); - userEvent.click(screen.getByTestId('upload-azure')); - userEvent.click(screen.getByTestId('upload-google')); - userEvent.click(screen.getByTestId('checkbox-vmware')); - userEvent.click(screen.getByTestId('checkbox-guest-image')); - userEvent.click(screen.getByTestId('checkbox-image-installer')); + userEvent.click(screen.getByTestId('upload-aws')); + userEvent.click(screen.getByTestId('upload-azure')); + userEvent.click(screen.getByTestId('upload-google')); + userEvent.click(screen.getByTestId('checkbox-vmware')); + userEvent.click(screen.getByTestId('checkbox-guest-image')); + userEvent.click(screen.getByTestId('checkbox-image-installer')); - screen.getByRole('button', { name: /Next/ }).click(); - userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); - screen.getByRole('button', { name: /Next/ }).click(); + screen.getByRole('button', { name: /Next/ }).click(); + userEvent.type(screen.getByTestId('aws-account-id'), '012345678901'); + screen.getByRole('button', { name: /Next/ }).click(); - userEvent.type(screen.getByTestId('input-google-email'), 'test@test.com'); - screen.getByRole('button', { name: /Next/ }).click(); + userEvent.type(screen.getByTestId('input-google-email'), 'test@test.com'); + screen.getByRole('button', { name: /Next/ }).click(); - // Randomly generated GUID - userEvent.type(screen.getByTestId('azure-tenant-id'), 'b8f86d22-4371-46ce-95e7-65c415f3b1e2'); - userEvent.type(screen.getByTestId('azure-subscription-id'), '60631143-a7dc-4d15-988b-ba83f3c99711'); - userEvent.type(screen.getByTestId('azure-resource-group'), 'testResourceGroup'); - screen.getByRole('button', { name: /Next/ }).click(); + // Randomly generated GUID + userEvent.type( + screen.getByTestId('azure-tenant-id'), + 'b8f86d22-4371-46ce-95e7-65c415f3b1e2' + ); + userEvent.type( + screen.getByTestId('azure-subscription-id'), + '60631143-a7dc-4d15-988b-ba83f3c99711' + ); + userEvent.type( + screen.getByTestId('azure-resource-group'), + 'testResourceGroup' + ); + screen.getByRole('button', { name: /Next/ }).click(); - // registration - const mockActivationKeys = [{ id: '0', name: 'name0' }, { id: 1, name: 'name1' }]; - jest - .spyOn(api, 'getActivationKeys') - .mockImplementation(() => Promise.resolve(mockActivationKeys)); + // registration + const mockActivationKeys = [ + { id: '0', name: 'name0' }, + { id: 1, name: 'name1' }, + ]; + jest + .spyOn(api, 'getActivationKeys') + .mockImplementation(() => Promise.resolve(mockActivationKeys)); - const registrationRadio = screen.getByLabelText('Register and connect image instances with Red Hat'); - userEvent.click(registrationRadio); + const registrationRadio = screen.getByLabelText( + 'Register and connect image instances with Red Hat' + ); + userEvent.click(registrationRadio); - const activationKeyDropdown = await screen.findByRole('textbox', { - name: 'Select activation key' - }); - userEvent.click(activationKeyDropdown); - const activationKey = await screen.findByRole('option', { - name: 'name0' - }); - userEvent.click(activationKey); - screen.getByDisplayValue('name0'); + const activationKeyDropdown = await screen.findByRole('textbox', { + name: 'Select activation key', + }); + userEvent.click(activationKeyDropdown); + const activationKey = await screen.findByRole('option', { + name: 'name0', + }); + userEvent.click(activationKey); + screen.getByDisplayValue('name0'); - next.click(); + next.click(); - // fsc - const toggle = await screen.findByTestId('file-system-config-toggle-manual'); - within(toggle).getByRole('button').click(); - const ap = await screen.findByTestId('file-system-add-partition'); - ap.click(); - ap.click(); - const tbody = screen.getByTestId('file-system-configuration-tbody'); - const rows = within(tbody).getAllByRole('row'); - expect(rows).toHaveLength(3); - // set mountpoint of final row to /var/tmp - within(rows[2]).getAllByRole('button', { name: 'Options menu' })[0].click(); - within(rows[2]).getByRole('option', { name: '/var' }).click(); - await waitForElementToBeRemoved(() => screen.queryAllByRole('heading', { name: 'Danger alert: Duplicate mount point.' })); - userEvent.type(within(rows[2]).getByRole('textbox', { name: 'Mount point suffix text input' }), '/tmp'); + // fsc + const toggle = await screen.findByTestId( + 'file-system-config-toggle-manual' + ); + within(toggle).getByRole('button').click(); + const ap = await screen.findByTestId('file-system-add-partition'); + ap.click(); + ap.click(); + const tbody = screen.getByTestId('file-system-configuration-tbody'); + const rows = within(tbody).getAllByRole('row'); + expect(rows).toHaveLength(3); + // set mountpoint of final row to /var/tmp + within(rows[2]).getAllByRole('button', { name: 'Options menu' })[0].click(); + within(rows[2]).getByRole('option', { name: '/var' }).click(); + await waitForElementToBeRemoved(() => + screen.queryAllByRole('heading', { + name: 'Danger alert: Duplicate mount point.', + }) + ); + userEvent.type( + within(rows[2]).getByRole('textbox', { + name: 'Mount point suffix text input', + }), + '/tmp' + ); - // set size of the final row to 100 MiB - userEvent.type(within(rows[2]).getByRole('textbox', { name: 'Size text input' }), '{backspace}100'); - within(rows[2]).getAllByRole('button', { name: 'Options menu' })[1].click(); - within(rows[2]).getByRole('option', { name: 'MiB' }).click(); - next.click(); + // set size of the final row to 100 MiB + userEvent.type( + within(rows[2]).getByRole('textbox', { name: 'Size text input' }), + '{backspace}100' + ); + within(rows[2]).getAllByRole('button', { name: 'Options menu' })[1].click(); + within(rows[2]).getByRole('option', { name: 'MiB' }).click(); + next.click(); - // packages - const getPackages = jest - .spyOn(api, 'getPackages') - .mockImplementation(() => Promise.resolve(mockPkgResult)); + // packages + const getPackages = jest + .spyOn(api, 'getPackages') + .mockImplementation(() => Promise.resolve(mockPkgResult)); - screen.getByText('Add optional additional packages to your image by searching available packages.'); - await searchForAvailablePackages(screen.getByTestId('search-available-pkgs-input'), 'test'); - expect(getPackages).toHaveBeenCalledTimes(1); - screen.getByRole('option', { name: /testPkg test package summary/ }).click(); - screen.getByRole('button', { name: /Add selected/ }).click(); - next.click(); + screen.getByText( + 'Add optional additional packages to your image by searching available packages.' + ); + await searchForAvailablePackages( + screen.getByTestId('search-available-pkgs-input'), + 'test' + ); + expect(getPackages).toHaveBeenCalledTimes(1); + screen + .getByRole('option', { name: /testPkg test package summary/ }) + .click(); + screen.getByRole('button', { name: /Add selected/ }).click(); + next.click(); - // Enter image name - const nameInput = screen.getByRole('textbox', { - name: 'Image name' - }); - userEvent.type(nameInput, 'MyImageName'); - next.click(); + // Enter image name + const nameInput = screen.getByRole('textbox', { + name: 'Image name', + }); + userEvent.type(nameInput, 'MyImageName'); + next.click(); - // review - await screen. - findByText('Review the information and click "Create image" to create the image using the following criteria.'); - await screen.findAllByText('Amazon Web Services'); - await screen.findAllByText('Google Cloud Platform'); - await screen.findByText('VMWare'); - await screen.findByText('Virtualization - Guest image'); - await screen.findByText('Bare metal - Installer'); - await screen.findByText('Register with Subscriptions and Red Hat Insights'); - await screen.findByText('MyImageName'); + // review + await screen.findByText( + 'Review the information and click "Create image" to create the image using the following criteria.' + ); + await screen.findAllByText('Amazon Web Services'); + await screen.findAllByText('Google Cloud Platform'); + await screen.findByText('VMWare'); + await screen.findByText('Virtualization - Guest image'); + await screen.findByText('Bare metal - Installer'); + await screen.findByText('Register with Subscriptions and Red Hat Insights'); + await screen.findByText('MyImageName'); - screen.getByTestId('file-system-configuration-popover').click(); - const revtbody = await screen.findByTestId('file-system-configuration-tbody-review'); - expect(within(revtbody).getAllByRole('row')).toHaveLength(3); + screen.getByTestId('file-system-configuration-popover').click(); + const revtbody = await screen.findByTestId( + 'file-system-configuration-tbody-review' + ); + expect(within(revtbody).getAllByRole('row')).toHaveLength(3); - await waitFor(() => { - const id = screen.getByTestId('organization-id'); - within(id).getByText(5); - }); - // mock the backend API - let ids = []; - const composeImage = jest - .spyOn(api, 'composeImage') - .mockImplementation(body => { - let id; - if (body.image_requests[0].upload_request.type === 'aws') { - expect(body).toEqual({ - distribution: RHEL_8, - image_name: 'MyImageName', - image_requests: [{ - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: { - share_with_accounts: [ '012345678901' ], - } - }, - }], - customizations: { - filesystem: [ - { - mountpoint: '/', - min_size: 10737418240, - }, - { - mountpoint: '/home', - min_size: 1073741824, - }, - { - mountpoint: '/var/tmp', - min_size: 104857600, - }, - ], - packages: [ 'testPkg' ], - subscription: { - 'activation-key': 'name0', - insights: true, - organization: 5, - 'server-url': 'subscription.rhsm.redhat.com', - 'base-url': 'https://cdn.redhat.com/' - }, - }, - }); - id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f56'; - } else if (body.image_requests[0].upload_request.type === 'gcp') { - expect(body).toEqual({ - distribution: RHEL_8, - image_name: 'MyImageName', - image_requests: [{ - architecture: 'x86_64', - image_type: 'gcp', - upload_request: { - type: 'gcp', - options: { - share_with_accounts: [ 'user:test@test.com' ], - } - }, - }], - customizations: { - filesystem: [ - { - mountpoint: '/', - min_size: 10737418240, - }, - { - mountpoint: '/home', - min_size: 1073741824, - }, - { - mountpoint: '/var/tmp', - min_size: 104857600, - }, - ], - packages: [ 'testPkg' ], - subscription: { - 'activation-key': 'name0', - insights: true, - organization: 5, - 'server-url': 'subscription.rhsm.redhat.com', - 'base-url': 'https://cdn.redhat.com/' - }, - }, - }); - id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f57'; - } else if (body.image_requests[0].upload_request.type === 'azure') { - expect(body).toEqual({ - distribution: RHEL_8, - image_name: 'MyImageName', - image_requests: [{ - architecture: 'x86_64', - image_type: 'vhd', - upload_request: { - type: 'azure', - options: { - tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2', - subscription_id: '60631143-a7dc-4d15-988b-ba83f3c99711', - resource_group: 'testResourceGroup', - } - }, - }], - customizations: { - filesystem: [ - { - mountpoint: '/', - min_size: 10737418240, - }, - { - mountpoint: '/home', - min_size: 1073741824, - }, - { - mountpoint: '/var/tmp', - min_size: 104857600, - }, - ], - packages: [ 'testPkg' ], - subscription: { - 'activation-key': 'name0', - insights: true, - organization: 5, - 'server-url': 'subscription.rhsm.redhat.com', - 'base-url': 'https://cdn.redhat.com/' - }, - }, - }); - id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f58'; - } else if (body.image_requests[0].image_type === 'vsphere') { - expect(body).toEqual({ - distribution: RHEL_8, - image_name: 'MyImageName', - image_requests: [{ - architecture: 'x86_64', - image_type: 'vsphere', - upload_request: { - type: 'aws.s3', - options: {} - }, - }], - customizations: { - filesystem: [ - { - mountpoint: '/', - min_size: 10737418240, - }, - { - mountpoint: '/home', - min_size: 1073741824, - }, - { - mountpoint: '/var/tmp', - min_size: 104857600, - }, - ], - packages: [ 'testPkg' ], - subscription: { - 'activation-key': 'name0', - insights: true, - organization: 5, - 'server-url': 'subscription.rhsm.redhat.com', - 'base-url': 'https://cdn.redhat.com/' - }, - }, - }); - id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f59'; - } else if (body.image_requests[0].image_type === 'guest-image') { - expect(body).toEqual({ - distribution: RHEL_8, - image_name: 'MyImageName', - image_requests: [{ - architecture: 'x86_64', - image_type: 'guest-image', - upload_request: { - type: 'aws.s3', - options: {} - }, - }], - customizations: { - filesystem: [ - { - mountpoint: '/', - min_size: 10737418240, - }, - { - mountpoint: '/home', - min_size: 1073741824, - }, - { - mountpoint: '/var/tmp', - min_size: 104857600, - }, - ], - packages: [ 'testPkg' ], - subscription: { - 'activation-key': 'name0', - insights: true, - organization: 5, - 'server-url': 'subscription.rhsm.redhat.com', - 'base-url': 'https://cdn.redhat.com/' - }, - }, - }); - id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a'; - } else if (body.image_requests[0].image_type === 'image-installer') { - expect(body).toEqual({ - distribution: RHEL_8, - image_name: 'MyImageName', - image_requests: [{ - architecture: 'x86_64', - image_type: 'image-installer', - upload_request: { - type: 'aws.s3', - options: {} - }, - }], - customizations: { - filesystem: [ - { - mountpoint: '/', - min_size: 10737418240, - }, - { - mountpoint: '/home', - min_size: 1073741824, - }, - { - mountpoint: '/var/tmp', - min_size: 104857600, - }, - ], - packages: [ 'testPkg' ], - subscription: { - 'activation-key': 'name0', - insights: true, - organization: 5, - 'server-url': 'subscription.rhsm.redhat.com', - 'base-url': 'https://cdn.redhat.com/' - }, - }, - }); - id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5b'; - } + await waitFor(() => { + const id = screen.getByTestId('organization-id'); + within(id).getByText(5); + }); + // mock the backend API + let ids = []; + const composeImage = jest + .spyOn(api, 'composeImage') + .mockImplementation((body) => { + let id; + if (body.image_requests[0].upload_request.type === 'aws') { + expect(body).toEqual({ + distribution: RHEL_8, + image_name: 'MyImageName', + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: { + share_with_accounts: ['012345678901'], + }, + }, + }, + ], + customizations: { + filesystem: [ + { + mountpoint: '/', + min_size: 10737418240, + }, + { + mountpoint: '/home', + min_size: 1073741824, + }, + { + mountpoint: '/var/tmp', + min_size: 104857600, + }, + ], + packages: ['testPkg'], + subscription: { + 'activation-key': 'name0', + insights: true, + organization: 5, + 'server-url': 'subscription.rhsm.redhat.com', + 'base-url': 'https://cdn.redhat.com/', + }, + }, + }); + id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f56'; + } else if (body.image_requests[0].upload_request.type === 'gcp') { + expect(body).toEqual({ + distribution: RHEL_8, + image_name: 'MyImageName', + image_requests: [ + { + architecture: 'x86_64', + image_type: 'gcp', + upload_request: { + type: 'gcp', + options: { + share_with_accounts: ['user:test@test.com'], + }, + }, + }, + ], + customizations: { + filesystem: [ + { + mountpoint: '/', + min_size: 10737418240, + }, + { + mountpoint: '/home', + min_size: 1073741824, + }, + { + mountpoint: '/var/tmp', + min_size: 104857600, + }, + ], + packages: ['testPkg'], + subscription: { + 'activation-key': 'name0', + insights: true, + organization: 5, + 'server-url': 'subscription.rhsm.redhat.com', + 'base-url': 'https://cdn.redhat.com/', + }, + }, + }); + id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f57'; + } else if (body.image_requests[0].upload_request.type === 'azure') { + expect(body).toEqual({ + distribution: RHEL_8, + image_name: 'MyImageName', + image_requests: [ + { + architecture: 'x86_64', + image_type: 'vhd', + upload_request: { + type: 'azure', + options: { + tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2', + subscription_id: '60631143-a7dc-4d15-988b-ba83f3c99711', + resource_group: 'testResourceGroup', + }, + }, + }, + ], + customizations: { + filesystem: [ + { + mountpoint: '/', + min_size: 10737418240, + }, + { + mountpoint: '/home', + min_size: 1073741824, + }, + { + mountpoint: '/var/tmp', + min_size: 104857600, + }, + ], + packages: ['testPkg'], + subscription: { + 'activation-key': 'name0', + insights: true, + organization: 5, + 'server-url': 'subscription.rhsm.redhat.com', + 'base-url': 'https://cdn.redhat.com/', + }, + }, + }); + id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f58'; + } else if (body.image_requests[0].image_type === 'vsphere') { + expect(body).toEqual({ + distribution: RHEL_8, + image_name: 'MyImageName', + image_requests: [ + { + architecture: 'x86_64', + image_type: 'vsphere', + upload_request: { + type: 'aws.s3', + options: {}, + }, + }, + ], + customizations: { + filesystem: [ + { + mountpoint: '/', + min_size: 10737418240, + }, + { + mountpoint: '/home', + min_size: 1073741824, + }, + { + mountpoint: '/var/tmp', + min_size: 104857600, + }, + ], + packages: ['testPkg'], + subscription: { + 'activation-key': 'name0', + insights: true, + organization: 5, + 'server-url': 'subscription.rhsm.redhat.com', + 'base-url': 'https://cdn.redhat.com/', + }, + }, + }); + id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f59'; + } else if (body.image_requests[0].image_type === 'guest-image') { + expect(body).toEqual({ + distribution: RHEL_8, + image_name: 'MyImageName', + image_requests: [ + { + architecture: 'x86_64', + image_type: 'guest-image', + upload_request: { + type: 'aws.s3', + options: {}, + }, + }, + ], + customizations: { + filesystem: [ + { + mountpoint: '/', + min_size: 10737418240, + }, + { + mountpoint: '/home', + min_size: 1073741824, + }, + { + mountpoint: '/var/tmp', + min_size: 104857600, + }, + ], + packages: ['testPkg'], + subscription: { + 'activation-key': 'name0', + insights: true, + organization: 5, + 'server-url': 'subscription.rhsm.redhat.com', + 'base-url': 'https://cdn.redhat.com/', + }, + }, + }); + id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a'; + } else if (body.image_requests[0].image_type === 'image-installer') { + expect(body).toEqual({ + distribution: RHEL_8, + image_name: 'MyImageName', + image_requests: [ + { + architecture: 'x86_64', + image_type: 'image-installer', + upload_request: { + type: 'aws.s3', + options: {}, + }, + }, + ], + customizations: { + filesystem: [ + { + mountpoint: '/', + min_size: 10737418240, + }, + { + mountpoint: '/home', + min_size: 1073741824, + }, + { + mountpoint: '/var/tmp', + min_size: 104857600, + }, + ], + packages: ['testPkg'], + subscription: { + 'activation-key': 'name0', + insights: true, + organization: 5, + 'server-url': 'subscription.rhsm.redhat.com', + 'base-url': 'https://cdn.redhat.com/', + }, + }, + }); + id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5b'; + } - ids.unshift(id); - return Promise.resolve({ id }); - }); + ids.unshift(id); + return Promise.resolve({ id }); + }); - const create = screen.getByRole('button', { name: /Create/ }); - create.click(); + const create = screen.getByRole('button', { name: /Create/ }); + create.click(); - // API request sent to backend - await expect(composeImage).toHaveBeenCalledTimes(6); + // API request sent to backend + await expect(composeImage).toHaveBeenCalledTimes(6); - // returns back to the landing page - await waitFor(() => expect(history.location.pathname).toBe('/')); - expect(store.getStore().getState().composes.allIds).toEqual(ids); + // returns back to the landing page + await waitFor(() => expect(history.location.pathname).toBe('/')); + expect(store.getStore().getState().composes.allIds).toEqual(ids); // set test timeout of 10 seconds - }, 10000); + }, 10000); }); describe('Keyboard accessibility', () => { - const setUp = async () => { - const view = renderWithReduxRouter(, {}, '/imagewizard'); - history = view.history; + const setUp = async () => { + const view = renderWithReduxRouter( + , + {}, + '/imagewizard' + ); + history = view.history; + }; + + const clickNext = () => { + const next = screen.getByRole('button', { name: /Next/ }); + next.click(); + }; + + const selectAllEnvironments = () => { + const awsTile = screen.getByTestId('upload-aws'); + awsTile.click(); + const googleTile = screen.getByTestId('upload-google'); + googleTile.click(); + const azureTile = screen.getByTestId('upload-azure'); + azureTile.click(); + const virtualizationCheckbox = screen.getByRole('checkbox', { + name: /virtualization guest image checkbox/i, + }); + virtualizationCheckbox.click(); + }; + + const fillAzureInputs = () => { + userEvent.type( + screen.getByTestId('azure-tenant-id'), + 'b8f86d22-4371-46ce-95e7-65c415f3b1e2' + ); + userEvent.type( + screen.getByTestId('azure-subscription-id'), + '60631143-a7dc-4d15-988b-ba83f3c99711' + ); + userEvent.type( + screen.getByTestId('azure-resource-group'), + 'testResourceGroup' + ); + }; + + test('autofocus on each step first input element', async () => { + setUp(); + + // Image output + selectAllEnvironments(); + clickNext(); + + // Target environment aws + const awsInput = screen.getByRole('textbox', { name: /aws account id/i }); + expect(awsInput).toHaveFocus(); + userEvent.type(awsInput, '012345678901'); + clickNext(); + + // Target environment google + const googleAccountRadio = screen.getByRole('radio', { + name: /google account/i, + }); + expect(googleAccountRadio).toHaveFocus(); + userEvent.type(screen.getByTestId('input-google-email'), 'test@test.com'); + clickNext(); + + // Target environment azure + const tenantIDInput = screen.getByTestId('azure-tenant-id'); + expect(tenantIDInput).toHaveFocus(); + fillAzureInputs(); + clickNext(); + + // Registration + const registerRadio = screen.getByRole('radio', { + name: /register and connect image instances with red hat/i, + }); + expect(registerRadio).toHaveFocus(); + await screen.findByRole('textbox', { + name: 'Select activation key', + }); + // skip registration + const registerLaterRadio = screen.getByLabelText('Register later'); + userEvent.click(registerLaterRadio); + + clickNext(); + + // File system configuration + clickNext(); + + // Packages + let availablePackagesInput; + // eslint-disable-next-line testing-library/no-unnecessary-act + await act(async () => { + const view = screen.getByTestId('search-available-pkgs-input'); + + availablePackagesInput = within(view).getByRole('textbox', { + name: /search input/i, + }); + }); + expect(availablePackagesInput).toHaveFocus(); + clickNext(); + + // Name + const nameInput = screen.getByRole('textbox', { name: /image name/i }); + expect(nameInput).toHaveFocus(); + clickNext(); + + // Review + const targetEnvironmentTab = screen.getByTestId('tab-target'); + expect(targetEnvironmentTab).toHaveFocus(); + }); + + test('pressing Esc closes the wizard', async () => { + setUp(); + // wizard needs to be interacted with for the esc key to work + const awsTile = screen.getByTestId('upload-aws'); + userEvent.click(awsTile); + userEvent.keyboard('{esc}'); + expect(history.location.pathname).toBe('/'); + }); + + test('pressing Enter does not advance the wizard', async () => { + setUp(); + const awsTile = screen.getByTestId('upload-aws'); + userEvent.click(awsTile); + userEvent.keyboard('{enter}'); + screen.getByRole('heading', { + name: /image output/i, + }); + }); + + test('target environment tiles are keyboard selectable', async () => { + const testTile = (tile) => { + tile.focus(); + userEvent.keyboard('{space}'); + expect(tile).toHaveClass('pf-m-selected'); + userEvent.keyboard('{space}'); + expect(tile).not.toHaveClass('pf-m-selected'); }; - const clickNext = () => { - const next = screen.getByRole('button', { name: /Next/ }); - next.click(); - }; + setUp(); + clickNext(); - const selectAllEnvironments = () => { - const awsTile = screen.getByTestId('upload-aws'); - awsTile.click(); - const googleTile = screen.getByTestId('upload-google'); - googleTile.click(); - const azureTile = screen.getByTestId('upload-azure'); - azureTile.click(); - const virtualizationCheckbox = screen.getByRole('checkbox', { - name: /virtualization guest image checkbox/i - }); - virtualizationCheckbox.click(); - }; - - const fillAzureInputs = () => { - userEvent.type(screen.getByTestId('azure-tenant-id'), 'b8f86d22-4371-46ce-95e7-65c415f3b1e2'); - userEvent.type(screen.getByTestId('azure-subscription-id'), '60631143-a7dc-4d15-988b-ba83f3c99711'); - userEvent.type(screen.getByTestId('azure-resource-group'), 'testResourceGroup'); - }; - - test('autofocus on each step first input element', async () => { - setUp(); - - // Image output - selectAllEnvironments(); - clickNext(); - - // Target environment aws - const awsInput = screen.getByRole('textbox', { name: /aws account id/i }); - expect(awsInput).toHaveFocus(); - userEvent.type(awsInput, '012345678901'); - clickNext(); - - // Target environment google - const googleAccountRadio = screen.getByRole('radio', { name: /google account/i }); - expect(googleAccountRadio).toHaveFocus(); - userEvent.type(screen.getByTestId('input-google-email'), 'test@test.com'); - clickNext(); - - // Target environment azure - const tenantIDInput = screen.getByTestId('azure-tenant-id'); - expect(tenantIDInput).toHaveFocus(); - fillAzureInputs(); - clickNext(); - - // Registration - const registerRadio = screen.getByRole('radio', { - name: /register and connect image instances with red hat/i - }); - expect(registerRadio).toHaveFocus(); - await screen.findByRole('textbox', { - name: 'Select activation key' - }); - // skip registration - const registerLaterRadio = screen.getByLabelText('Register later'); - userEvent.click(registerLaterRadio); - - clickNext(); - - // File system configuration - clickNext(); - - // Packages - let availablePackagesInput; - // eslint-disable-next-line testing-library/no-unnecessary-act - await act(async() => { - const view = screen.getByTestId('search-available-pkgs-input'); - - availablePackagesInput = within(view).getByRole('textbox', { - name: /search input/i - }); - }); - expect(availablePackagesInput).toHaveFocus(); - clickNext(); - - // Name - const nameInput = screen.getByRole('textbox', { name: /image name/i }); - expect(nameInput).toHaveFocus(); - clickNext(); - - // Review - const targetEnvironmentTab = screen.getByTestId('tab-target'); - expect(targetEnvironmentTab).toHaveFocus(); - }); - - test('pressing Esc closes the wizard', async () => { - setUp(); - // wizard needs to be interacted with for the esc key to work - const awsTile = screen.getByTestId('upload-aws'); - userEvent.click(awsTile); - userEvent.keyboard('{esc}'); - expect(history.location.pathname).toBe('/'); - }); - - test('pressing Enter does not advance the wizard', async () => { - setUp(); - const awsTile = screen.getByTestId('upload-aws'); - userEvent.click(awsTile); - userEvent.keyboard('{enter}'); - screen.getByRole('heading', { - name: /image output/i - }); - }); - - test('target environment tiles are keyboard selectable', async () => { - const testTile = (tile) => { - tile.focus(); - userEvent.keyboard('{space}'); - expect(tile).toHaveClass('pf-m-selected'); - userEvent.keyboard('{space}'); - expect(tile).not.toHaveClass('pf-m-selected'); - }; - - setUp(); - clickNext(); - - testTile(screen.getByTestId('upload-aws')); - testTile(screen.getByTestId('upload-google')); - testTile(screen.getByTestId('upload-azure')); - }); + testTile(screen.getByTestId('upload-aws')); + testTile(screen.getByTestId('upload-google')); + testTile(screen.getByTestId('upload-azure')); + }); }); diff --git a/src/test/Components/ImagesTable/ImagesTable.test.js b/src/test/Components/ImagesTable/ImagesTable.test.js index 0c12c82a..99ab2239 100644 --- a/src/test/Components/ImagesTable/ImagesTable.test.js +++ b/src/test/Components/ImagesTable/ImagesTable.test.js @@ -10,471 +10,498 @@ import { RHEL_8 } from '../../../constants.js'; import userEvent from '@testing-library/user-event'; jest.mock('../../../store/actions/actions', () => { - return { - composesGet: () => ({ type: 'foo' }), - composeGetStatus: () => ({ type: 'bar' }) - }; + return { + composesGet: () => ({ type: 'foo' }), + composeGetStatus: () => ({ type: 'bar' }), + }; }); const store = { - composes: { - errors: null, - allIds: [ - 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa', - 'edbae1c2-62bc-42c1-ae0c-3110ab718f58', - '42ad0826-30b5-4f64-a24e-957df26fd564', - '955944a2-e149-4058-8ac1-35b514cb5a16', - 'f7a60094-b376-4b58-a102-5c8c82dfd18b', - '1579d95b-8f1d-4982-8c53-8c2afa4ab04c', - '61b0effa-c901-4ee5-86b9-2010b47f1b22', - 'ca03f120-9840-4959-871e-94a5cb49d1f2', - '551de6f6-1533-4b46-a69f-7924051f9bc6', - '77fa8b03-7efb-4120-9a20-da66d68c4494', - ], - byId: { - '1579d95b-8f1d-4982-8c53-8c2afa4ab04c': { - id: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c', - image_name: 'testImageName', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: {} - } - } - ], - }, - image_status: { - status: 'success', - upload_status: { - options: { - ami: 'ami-0217b81d9be50e44b', - region: 'us-east-1' - }, - status: 'success', - type: 'aws' - } - }, + composes: { + errors: null, + allIds: [ + 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa', + 'edbae1c2-62bc-42c1-ae0c-3110ab718f58', + '42ad0826-30b5-4f64-a24e-957df26fd564', + '955944a2-e149-4058-8ac1-35b514cb5a16', + 'f7a60094-b376-4b58-a102-5c8c82dfd18b', + '1579d95b-8f1d-4982-8c53-8c2afa4ab04c', + '61b0effa-c901-4ee5-86b9-2010b47f1b22', + 'ca03f120-9840-4959-871e-94a5cb49d1f2', + '551de6f6-1533-4b46-a69f-7924051f9bc6', + '77fa8b03-7efb-4120-9a20-da66d68c4494', + ], + byId: { + '1579d95b-8f1d-4982-8c53-8c2afa4ab04c': { + id: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c', + image_name: 'testImageName', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: {}, + }, }, - // kept "running" for backward compatibility - 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa': { - id: 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: {} - } - } - ], - }, - image_status: { - status: 'running', - }, + ], + }, + image_status: { + status: 'success', + upload_status: { + options: { + ami: 'ami-0217b81d9be50e44b', + region: 'us-east-1', }, - 'edbae1c2-62bc-42c1-ae0c-3110ab718f58': { - id: 'edbae1c2-62bc-42c1-ae0c-3110ab718f58', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: {} - } - } - ], - }, - image_status: { - status: 'pending', - }, + status: 'success', + type: 'aws', + }, + }, + }, + // kept "running" for backward compatibility + 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa': { + id: 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: {}, + }, }, - '42ad0826-30b5-4f64-a24e-957df26fd564': { - id: '42ad0826-30b5-4f64-a24e-957df26fd564', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: {} - } - } - ], - }, - image_status: { - status: 'building', - }, + ], + }, + image_status: { + status: 'running', + }, + }, + 'edbae1c2-62bc-42c1-ae0c-3110ab718f58': { + id: 'edbae1c2-62bc-42c1-ae0c-3110ab718f58', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: {}, + }, }, - '955944a2-e149-4058-8ac1-35b514cb5a16': { - id: '955944a2-e149-4058-8ac1-35b514cb5a16', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: {} - } - } - ], - }, - image_status: { - status: 'uploading', - }, + ], + }, + image_status: { + status: 'pending', + }, + }, + '42ad0826-30b5-4f64-a24e-957df26fd564': { + id: '42ad0826-30b5-4f64-a24e-957df26fd564', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: {}, + }, }, - 'f7a60094-b376-4b58-a102-5c8c82dfd18b': { - id: 'f7a60094-b376-4b58-a102-5c8c82dfd18b', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: {} - } - } - ], - }, - image_status: { - status: 'registering', - }, + ], + }, + image_status: { + status: 'building', + }, + }, + '955944a2-e149-4058-8ac1-35b514cb5a16': { + id: '955944a2-e149-4058-8ac1-35b514cb5a16', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: {}, + }, }, - '61b0effa-c901-4ee5-86b9-2010b47f1b22': { - id: '61b0effa-c901-4ee5-86b9-2010b47f1b22', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: {} - } - } - ], - }, - image_status: { - status: 'failure', - error: { - reason: 'A dependency error occured', - details: { - reason: 'Error in depsolve job' - } - } - }, + ], + }, + image_status: { + status: 'uploading', + }, + }, + 'f7a60094-b376-4b58-a102-5c8c82dfd18b': { + id: 'f7a60094-b376-4b58-a102-5c8c82dfd18b', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: {}, + }, }, - 'ca03f120-9840-4959-871e-94a5cb49d1f2': { - id: 'ca03f120-9840-4959-871e-94a5cb49d1f2', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'vhd', - upload_request: { - type: 'gcp', - options: { - share_with_accounts: [ - 'serviceAccount:test@email.com' - ] - } - } - } - ], - }, - image_status: { - status: 'success', - upload_status: { - options: { - image_name: 'composer-api-d446d8cb-7c16-4756-bf7d-706293785b05', - project_id: 'red-hat-image-builder' - }, - status: 'success', - type: 'gcp' - } - }, + ], + }, + image_status: { + status: 'registering', + }, + }, + '61b0effa-c901-4ee5-86b9-2010b47f1b22': { + id: '61b0effa-c901-4ee5-86b9-2010b47f1b22', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: {}, + }, }, - '551de6f6-1533-4b46-a69f-7924051f9bc6': { - id: '551de6f6-1533-4b46-a69f-7924051f9bc6', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'vhd', - upload_request: { - type: 'azure', - options: {} - } - } - ], - }, - image_status: { - status: 'building', - }, + ], + }, + image_status: { + status: 'failure', + error: { + reason: 'A dependency error occured', + details: { + reason: 'Error in depsolve job', }, - '77fa8b03-7efb-4120-9a20-da66d68c4494': { - id: '77fa8b03-7efb-4120-9a20-da66d68c4494', - created_at: '2021-04-27 12:31:12.794809 +0000 UTC', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'vhd', - upload_request: { - type: 'azure', - options: { - tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2', - subscription_id: 'test-subscription-id', - resource_group: 'test-resource-group' - } - } - } - ], - }, - image_status: { - status: 'success', - upload_status: { - options: { - image_name: 'composer-api-cc5920c3-5451-4282-aab3-725d3df7f1cb' - }, - status: 'success', - type: 'azure' - } + }, + }, + }, + 'ca03f120-9840-4959-871e-94a5cb49d1f2': { + id: 'ca03f120-9840-4959-871e-94a5cb49d1f2', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'vhd', + upload_request: { + type: 'gcp', + options: { + share_with_accounts: ['serviceAccount:test@email.com'], }, + }, }, - 'b7193673-8dcc-4a5f-ac30-e9f4940d8346': { - created_at: '2022-01-11 13:33:33.767002 +0000 UTC', - id: 'b7193673-8dcc-4a5f-ac30-e9f4940d8346', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'vsphere', - upload_request: { - options: {}, - type: 'aws.s3' - } - } - ] - }, - image_status: { - status: 'success', - upload_status: { - options: { - url: 'https://s3.amazonaws.com/b7193673-8dcc-4a5f-ac30-e9f4940d8346-disk.vmdk' - }, - status: 'success', - type: 'aws.s3' - } - } + ], + }, + image_status: { + status: 'success', + upload_status: { + options: { + image_name: 'composer-api-d446d8cb-7c16-4756-bf7d-706293785b05', + project_id: 'red-hat-image-builder', }, - '4873fd0f-1851-4b9f-b4fe-4639fce90794': { - created_at: '2022-01-11 13:33:33.767002 +0000 UTC', - id: '4873fd0f-1851-4b9f-b4fe-4639fce90793', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'image-installer', - upload_request: { - options: {}, - type: 'aws.s3' - } - } - ] - }, - image_status: { - status: 'success', - upload_status: { - options: { - url: 'https://s3.amazonaws.com/4873fd0f-1851-4b9f-b4fe-4639fce90794-installer.iso' - }, - status: 'success', - type: 'aws.s3' - } - } + status: 'success', + type: 'gcp', + }, + }, + }, + '551de6f6-1533-4b46-a69f-7924051f9bc6': { + id: '551de6f6-1533-4b46-a69f-7924051f9bc6', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'vhd', + upload_request: { + type: 'azure', + options: {}, + }, }, - '7b7d0d51-7106-42ab-98f2-f89872a9d599': { - created_at: '2022-01-11 13:33:33.767002 +0000 UTC', - id: '7b7d0d51-7106-42ab-98f2-f89872a9d599', - request: { - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'guest-image', - upload_request: { - options: {}, - type: 'aws.s3' - } - } - ] + ], + }, + image_status: { + status: 'building', + }, + }, + '77fa8b03-7efb-4120-9a20-da66d68c4494': { + id: '77fa8b03-7efb-4120-9a20-da66d68c4494', + created_at: '2021-04-27 12:31:12.794809 +0000 UTC', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'vhd', + upload_request: { + type: 'azure', + options: { + tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2', + subscription_id: 'test-subscription-id', + resource_group: 'test-resource-group', }, - image_status: { - status: 'success', - upload_status: { - options: { - url: 'https://s3.amazonaws.com/7b7d0d51-7106-42ab-98f2-f89872a9d599-disk.qcow2' - }, - status: 'success', - type: 'aws.s3' - } - } + }, }, - } - } + ], + }, + image_status: { + status: 'success', + upload_status: { + options: { + image_name: 'composer-api-cc5920c3-5451-4282-aab3-725d3df7f1cb', + }, + status: 'success', + type: 'azure', + }, + }, + }, + 'b7193673-8dcc-4a5f-ac30-e9f4940d8346': { + created_at: '2022-01-11 13:33:33.767002 +0000 UTC', + id: 'b7193673-8dcc-4a5f-ac30-e9f4940d8346', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'vsphere', + upload_request: { + options: {}, + type: 'aws.s3', + }, + }, + ], + }, + image_status: { + status: 'success', + upload_status: { + options: { + url: 'https://s3.amazonaws.com/b7193673-8dcc-4a5f-ac30-e9f4940d8346-disk.vmdk', + }, + status: 'success', + type: 'aws.s3', + }, + }, + }, + '4873fd0f-1851-4b9f-b4fe-4639fce90794': { + created_at: '2022-01-11 13:33:33.767002 +0000 UTC', + id: '4873fd0f-1851-4b9f-b4fe-4639fce90793', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'image-installer', + upload_request: { + options: {}, + type: 'aws.s3', + }, + }, + ], + }, + image_status: { + status: 'success', + upload_status: { + options: { + url: 'https://s3.amazonaws.com/4873fd0f-1851-4b9f-b4fe-4639fce90794-installer.iso', + }, + status: 'success', + type: 'aws.s3', + }, + }, + }, + '7b7d0d51-7106-42ab-98f2-f89872a9d599': { + created_at: '2022-01-11 13:33:33.767002 +0000 UTC', + id: '7b7d0d51-7106-42ab-98f2-f89872a9d599', + request: { + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'guest-image', + upload_request: { + options: {}, + type: 'aws.s3', + }, + }, + ], + }, + image_status: { + status: 'success', + upload_status: { + options: { + url: 'https://s3.amazonaws.com/7b7d0d51-7106-42ab-98f2-f89872a9d599-disk.qcow2', + }, + status: 'success', + type: 'aws.s3', + }, + }, + }, + }, + }, }; describe('Images Table', () => { - test('render ImagesTable', () => { - renderWithReduxRouter(, store); - // make sure the empty-state message isn't present - const emptyState = screen.queryByTestId('empty-state'); - expect(emptyState).not.toBeInTheDocument(); + test('render ImagesTable', () => { + renderWithReduxRouter(, store); + // make sure the empty-state message isn't present + const emptyState = screen.queryByTestId('empty-state'); + expect(emptyState).not.toBeInTheDocument(); - // check table - const table = screen.getByTestId('images-table'); - const { getAllByRole } = within(table); - const rows = getAllByRole('row'); - // remove first row from list since it is just header labels - const header = rows.shift(); - // test the header has correct labels - expect(header.cells[1]).toHaveTextContent('Image name'); - expect(header.cells[2]).toHaveTextContent('Created'); - expect(header.cells[3]).toHaveTextContent('Release'); - expect(header.cells[4]).toHaveTextContent('Target'); - expect(header.cells[5]).toHaveTextContent('Status'); - expect(header.cells[6]).toHaveTextContent('Instance'); + // check table + const table = screen.getByTestId('images-table'); + const { getAllByRole } = within(table); + const rows = getAllByRole('row'); + // remove first row from list since it is just header labels + const header = rows.shift(); + // test the header has correct labels + expect(header.cells[1]).toHaveTextContent('Image name'); + expect(header.cells[2]).toHaveTextContent('Created'); + expect(header.cells[3]).toHaveTextContent('Release'); + expect(header.cells[4]).toHaveTextContent('Target'); + expect(header.cells[5]).toHaveTextContent('Status'); + expect(header.cells[6]).toHaveTextContent('Instance'); - // 10 rows for 10 images - expect(rows).toHaveLength(10); - for (const row of rows) { - const col1 = row.cells[1].textContent; + // 10 rows for 10 images + expect(rows).toHaveLength(10); + for (const row of rows) { + const col1 = row.cells[1].textContent; - const composes = Object.values(store.composes.byId); - // find compose with either the user defined image name or the uuid - const compose = composes.find(compose => compose?.image_name === col1 || compose.id === col1); - expect(compose).toBeTruthy(); + const composes = Object.values(store.composes.byId); + // find compose with either the user defined image name or the uuid + const compose = composes.find( + (compose) => compose?.image_name === col1 || compose.id === col1 + ); + expect(compose).toBeTruthy(); - // date should match the month day and year of the timestamp. - expect(row.cells[2]).toHaveTextContent('Apr 27, 2021'); + // date should match the month day and year of the timestamp. + expect(row.cells[2]).toHaveTextContent('Apr 27, 2021'); - // render the expected and compare the text content - let testElement = document.createElement('testElement'); - render(, { container: testElement }); - expect(row.cells[4]).toHaveTextContent(testElement.textContent); + // render the expected and compare the text content + let testElement = document.createElement('testElement'); + render( + , + { container: testElement } + ); + expect(row.cells[4]).toHaveTextContent(testElement.textContent); - // render the expected and compare the text content - render(, { container: testElement }); - expect(row.cells[5]).toHaveTextContent(testElement.textContent); + // render the expected and compare the text content + render(, { + container: testElement, + }); + expect(row.cells[5]).toHaveTextContent(testElement.textContent); - // render the expected and compare the text content for a link - render( - , - { container: testElement } - ); - expect(row.cells[6]).toHaveTextContent(testElement.textContent); - } + // render the expected and compare the text content for a link + render( + , + { container: testElement } + ); + expect(row.cells[6]).toHaveTextContent(testElement.textContent); + } + }); + + test('check recreate action', () => { + const { history } = renderWithReduxRouter(, store); + + // get rows + const table = screen.getByTestId('images-table'); + const { getAllByRole } = within(table); + const rows = getAllByRole('row'); + + // first row is header so look at index 1 + const imageId = rows[1].cells[1].textContent; + + const actionsButton = within(rows[1]).getByRole('button', { + name: 'Actions', + }); + userEvent.click(actionsButton); + const recreateButton = screen.getByRole('button', { + name: 'Recreate image', + }); + userEvent.click(recreateButton); + + expect(history.location.pathname).toBe('/imagewizard'); + expect(history.location.state.composeRequest).toStrictEqual( + store.composes.byId[imageId].request + ); + expect(history.location.state.initialStep).toBe('review'); + }); + + test('check expandable row toggle', () => { + renderWithReduxRouter(, store); + + const table = screen.getByTestId('images-table'); + const { getAllByRole } = within(table); + const rows = getAllByRole('row'); + + const toggleButton = within(rows[6]).getByRole('button', { + name: /details/i, }); - test('check recreate action', () => { - const { history } = renderWithReduxRouter(, store); + expect( + screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1] + ).not.toBeVisible(); + userEvent.click(toggleButton); + expect( + screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1] + ).toBeVisible(); + userEvent.click(toggleButton); + expect( + screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1] + ).not.toBeVisible(); + }); - // get rows - const table = screen.getByTestId('images-table'); - const { getAllByRole } = within(table); - const rows = getAllByRole('row'); + test('check error details', () => { + renderWithReduxRouter(, store); - // first row is header so look at index 1 - const imageId = rows[1].cells[1].textContent; + const table = screen.getByTestId('images-table'); + const { getAllByRole } = within(table); + const rows = getAllByRole('row'); - const actionsButton = within(rows[1]).getByRole('button', { - name: 'Actions' - }); - userEvent.click(actionsButton); - const recreateButton = screen.getByRole('button', { - name: 'Recreate image' - }); - userEvent.click(recreateButton); - - expect(history.location.pathname).toBe('/imagewizard'); - expect(history.location.state.composeRequest).toStrictEqual(store.composes.byId[imageId].request); - expect(history.location.state.initialStep).toBe('review'); + const errorToggle = within(rows[7]).getByRole('button', { + name: /details/i, }); - test('check expandable row toggle', () => { - renderWithReduxRouter(, store); + expect( + screen.getAllByText(/61b0effa-c901-4ee5-86b9-2010b47f1b22/i)[1] + ).not.toBeVisible(); + userEvent.click(errorToggle); - const table = screen.getByTestId('images-table'); - const { getAllByRole } = within(table); - const rows = getAllByRole('row'); - - const toggleButton = within(rows[6]).getByRole('button', { name: /details/i }); - - expect(screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]).not.toBeVisible(); - userEvent.click(toggleButton); - expect(screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]).toBeVisible(); - userEvent.click(toggleButton); - expect(screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]).not.toBeVisible(); - }); - - test('check error details', () => { - renderWithReduxRouter(, store); - - const table = screen.getByTestId('images-table'); - const { getAllByRole } = within(table); - const rows = getAllByRole('row'); - - const errorToggle = within(rows[7]).getByRole('button', { name: /details/i }); - - expect(screen.getAllByText(/61b0effa-c901-4ee5-86b9-2010b47f1b22/i)[1]).not.toBeVisible(); - userEvent.click(errorToggle); - - expect(screen.getAllByText(/61b0effa-c901-4ee5-86b9-2010b47f1b22/i)[1]).toBeVisible(); - expect(screen.getAllByText(/Error in depsolve job/i)[0]).toBeVisible(); - }); + expect( + screen.getAllByText(/61b0effa-c901-4ee5-86b9-2010b47f1b22/i)[1] + ).toBeVisible(); + expect(screen.getAllByText(/Error in depsolve job/i)[0]).toBeVisible(); + }); }); describe('Images Table Toolbar', () => { - test('render toolbar', () => { - renderWithReduxRouter(, store); - // check create image button - screen.getByTestId('create-image-action'); + test('render toolbar', () => { + renderWithReduxRouter(, store); + // check create image button + screen.getByTestId('create-image-action'); - // check pagination renders - screen.getByTestId('images-pagination'); - }); + // check pagination renders + screen.getByTestId('images-pagination'); + }); }); diff --git a/src/test/Components/LandingPage/LandingPage.test.js b/src/test/Components/LandingPage/LandingPage.test.js index c97b79e0..fc6d3fd6 100644 --- a/src/test/Components/LandingPage/LandingPage.test.js +++ b/src/test/Components/LandingPage/LandingPage.test.js @@ -5,28 +5,28 @@ import LandingPage from '../../../Components/LandingPage/LandingPage'; import api from '../../../api.js'; jest.mock('../../../store/actions/actions', () => { - return { - composesGet: () => ({ type: 'foo' }), - composeGetStatus: () => ({ type: 'bar' }) - }; + return { + composesGet: () => ({ type: 'foo' }), + composeGetStatus: () => ({ type: 'bar' }), + }; }); describe('Landing Page', () => { - test('renders page heading', async () => { - renderWithReduxRouter(); + test('renders page heading', async () => { + renderWithReduxRouter(); - const composeImage = jest.spyOn(api, 'getVersion'); - composeImage.mockResolvedValue({ version: '1.0' }); - // check heading - screen.getByRole('heading', { name: /Image Builder/i }); - }); + const composeImage = jest.spyOn(api, 'getVersion'); + composeImage.mockResolvedValue({ version: '1.0' }); + // check heading + screen.getByRole('heading', { name: /Image Builder/i }); + }); - test('renders EmptyState child component', async () => { - renderWithReduxRouter(); + test('renders EmptyState child component', async () => { + renderWithReduxRouter(); - // check action loads - screen.getByTestId('create-image-action'); - // check table loads - screen.getByTestId('empty-state'); - }); + // check action loads + screen.getByTestId('create-image-action'); + // check table loads + screen.getByTestId('empty-state'); + }); }); diff --git a/src/test/redux/actions.test.js b/src/test/redux/actions.test.js index c96a6030..0d5717d3 100644 --- a/src/test/redux/actions.test.js +++ b/src/test/redux/actions.test.js @@ -3,32 +3,32 @@ import types from '../../store/types'; import { RHEL_8 } from '../../constants.js'; const compose = { - '77e4c693-0497-4b85-936d-b2a3ad69571b': { - id: '77e4c693-0497-4b85-936d-b2a3ad69571b', - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: {} - } - } - ], - image_status: { - status: 'uploading', + '77e4c693-0497-4b85-936d-b2a3ad69571b': { + id: '77e4c693-0497-4b85-936d-b2a3ad69571b', + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: {}, }, - } + }, + ], + image_status: { + status: 'uploading', + }, + }, }; describe('composeUpdated', () => { - test('returns dict', () => { - const result = actions.composeUpdated(compose); + test('returns dict', () => { + const result = actions.composeUpdated(compose); - // this function updates the type attribute and - // returns everything else unchanged - expect(result.type).toBe(types.COMPOSE_UPDATED); - expect(result.payload.compose).toBe(compose); - }); + // this function updates the type attribute and + // returns everything else unchanged + expect(result.type).toBe(types.COMPOSE_UPDATED); + expect(result.payload.compose).toBe(compose); + }); }); diff --git a/src/test/redux/reducers.test.js b/src/test/redux/reducers.test.js index 1b16dbdd..ab870d6a 100644 --- a/src/test/redux/reducers.test.js +++ b/src/test/redux/reducers.test.js @@ -3,109 +3,105 @@ import types from '../../store/types'; import { RHEL_8 } from '../../constants.js'; const compose = { - id: '77e4c693-0497-4b85-936d-b2a3ad69571b', - distribution: RHEL_8, - image_requests: [ - { - architecture: 'x86_64', - image_type: 'ami', - upload_request: { - type: 'aws', - options: {} - } - } - ], - image_status: { - status: 'uploading', + id: '77e4c693-0497-4b85-936d-b2a3ad69571b', + distribution: RHEL_8, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'ami', + upload_request: { + type: 'aws', + options: {}, + }, }, + ], + image_status: { + status: 'uploading', + }, }; describe('composes', () => { - test('returns state for unknown actions', () => { - const result = composes({}, { - type: 'THIS-IS-UNKNOWN', - }); + test('returns state for unknown actions', () => { + const result = composes( + {}, + { + type: 'THIS-IS-UNKNOWN', + } + ); - expect(result).toEqual({}); + expect(result).toEqual({}); + }); + + test('returns updated state for types.COMPOSE_ADDED', () => { + const state = { + allIds: [], + byId: {}, + count: 1, + errors: null, + }; + const result = composes(state, { + type: types.COMPOSE_ADDED, + payload: { compose }, }); - test('returns updated state for types.COMPOSE_ADDED', () => { - const state = { - allIds: [], - byId: {}, - count: 1, - errors: null, - }; - const result = composes(state, { - type: types.COMPOSE_ADDED, - payload: { compose } - }); + expect(result.allIds).toEqual(['77e4c693-0497-4b85-936d-b2a3ad69571b']); + expect(result.byId['77e4c693-0497-4b85-936d-b2a3ad69571b']).toEqual( + compose + ); + expect(result.count).toEqual(1); + expect(result.error).toEqual(null); + }); - expect(result.allIds) - .toEqual([ '77e4c693-0497-4b85-936d-b2a3ad69571b' ]); - expect(result.byId['77e4c693-0497-4b85-936d-b2a3ad69571b']) - .toEqual(compose); - expect(result.count) - .toEqual(1); - expect(result.error) - .toEqual(null); + test('returns updated state for types.COMPOSE_UPDATED', () => { + const state = { + allIds: ['77e4c693-0497-4b85-936d-b2a3ad69571b'], + byId: { + '77e4c693-0497-4b85-936d-b2a3ad69571b': {}, + }, + count: 2, + error: null, + }; + const result = composes(state, { + type: types.COMPOSE_UPDATED, + payload: { compose }, }); - test('returns updated state for types.COMPOSE_UPDATED', () => { - const state = { - allIds: [ '77e4c693-0497-4b85-936d-b2a3ad69571b' ], - byId: { - '77e4c693-0497-4b85-936d-b2a3ad69571b': {}, - }, - count: 2, - error: null, - }; - const result = composes(state, { - type: types.COMPOSE_UPDATED, - payload: { compose } - }); + expect(result.allIds).toEqual(['77e4c693-0497-4b85-936d-b2a3ad69571b']); + expect(result.byId['77e4c693-0497-4b85-936d-b2a3ad69571b']).toEqual( + compose + ); + expect(result.count).toEqual(2); + expect(result.error).toEqual(null); + }); - expect(result.allIds) - .toEqual([ '77e4c693-0497-4b85-936d-b2a3ad69571b' ]); - expect(result.byId['77e4c693-0497-4b85-936d-b2a3ad69571b']) - .toEqual(compose); - expect(result.count) - .toEqual(2); - expect(result.error) - .toEqual(null); + test('returns updated state for types.COMPOSE_FAILED', () => { + const state = { + allIds: [], + byId: {}, + count: 0, + error: null, + }; + const result = composes(state, { + type: types.COMPOSE_FAILED, + payload: { error: 'test error' }, }); - test('returns updated state for types.COMPOSE_FAILED', () => { - const state = { - allIds: [], - byId: {}, - count: 0, - error: null, - }; - const result = composes(state, { - type: types.COMPOSE_FAILED, - payload: { error: 'test error' } - }); + expect(result.error).toEqual('test error'); + }); - expect(result.error) - .toEqual('test error'); + test('returns updated state for types.COMPOSES_UPDATED_COUNT', () => { + const state = { + allIds: [], + byId: {}, + count: 0, + error: null, + }; + + const result = composes(state, { + type: types.COMPOSES_UPDATED_COUNT, + payload: { count: 1 }, }); - test('returns updated state for types.COMPOSES_UPDATED_COUNT', () => { - const state = { - allIds: [], - byId: {}, - count: 0, - error: null, - }; - - const result = composes(state, { - type: types.COMPOSES_UPDATED_COUNT, - payload: { count: 1 } - }); - - expect(result.count) - .toEqual(1); - - }); + expect(result.count).toEqual(1); + }); }); diff --git a/src/test/testUtils.js b/src/test/testUtils.js index 909cf65f..4eb1761d 100644 --- a/src/test/testUtils.js +++ b/src/test/testUtils.js @@ -6,16 +6,18 @@ import { createMemoryHistory } from 'history'; import { init, clearStore } from '../store'; export const renderWithReduxRouter = (component, store = {}, route = '/') => { - const history = createMemoryHistory({ initialEntries: [ route ]}); - clearStore(); - let reduxStore = init(store); - return { - ...render( - - {component} - - ), - history, - reduxStore - }; + const history = createMemoryHistory({ initialEntries: [route] }); + clearStore(); + let reduxStore = init(store); + return { + ...render( + + + {component} + + + ), + history, + reduxStore, + }; };