Wizard: Drop the WizardV1
This commit is contained in:
parent
54d09d636e
commit
5fcc80d2db
75 changed files with 103 additions and 11588 deletions
|
|
@ -184,7 +184,7 @@ https://github.com/RedHatInsights/image-builder-frontend/blob/c84b493eba82ce83a7
|
|||
##### Mocking flags for tests
|
||||
|
||||
Flags can be mocked for the unit tests to access some feature. Checkout:
|
||||
https://github.com/RedHatInsights/image-builder-frontend/blob/c84b493eba82ce83a7844943943d91112ffe8322/src/test/Components/CreateImageWizard/CreateImageWizard.test.js#L74
|
||||
https://github.com/osbuild/image-builder-frontend/blob/9a464e416bc3769cfc8e23b62f1dd410eb0e0455/src/test/Components/CreateImageWizardV2/CreateImageWizard.test.tsx#L49
|
||||
|
||||
If the two possible code path accessible via the toggles are defined in the code
|
||||
base, then it's good practice to test the two of them. If not, only test what's
|
||||
|
|
|
|||
|
|
@ -106,29 +106,6 @@ if (process.env.MSW) {
|
|||
plugins[definePluginIndex] = newDefinePlugin;
|
||||
}
|
||||
|
||||
if (process.env.EXPERIMENTAL) {
|
||||
/*
|
||||
We would like the client to be able to determine whether or not it is in
|
||||
experimental mode at run time based on the value of process.env.EXPERIMENTAL.
|
||||
We can add that variable to process.env via the DefinesPlugin plugin, but
|
||||
DefinePlugin has already been added by config() to the default webpackConfig.
|
||||
|
||||
Therefore, we find it in the `plugins` array based on its type, then update
|
||||
it to add our new process.env.EXPERIMENTAL variable.
|
||||
*/
|
||||
const definePluginIndex = plugins.findIndex(
|
||||
(plugin) => plugin instanceof webpack.DefinePlugin
|
||||
);
|
||||
const definePlugin = plugins[definePluginIndex];
|
||||
|
||||
const newDefinePlugin = new webpack.DefinePlugin({
|
||||
...definePlugin.definitions,
|
||||
'process.env.EXPERIMENTAL': true,
|
||||
});
|
||||
|
||||
plugins[definePluginIndex] = newDefinePlugin;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
...webpackConfig,
|
||||
plugins,
|
||||
|
|
|
|||
|
|
@ -127,9 +127,7 @@
|
|||
"prod-stable": "PROXY=true webpack serve --config config/dev.webpack.config.js",
|
||||
"stage-stable": "STAGE=true npm run prod-stable",
|
||||
"stage-beta": "STAGE=true npm run prod-beta",
|
||||
"stage-beta:experimental": "EXPERIMENTAL=TRUE npm run stage-beta",
|
||||
"stage-beta:msw": "MSW=TRUE npm run stage-beta",
|
||||
"stage-beta:msw+experimental": "EXPERIMENTAL=TRUE npm run stage-beta:msw",
|
||||
"test": "TZ=UTC jest --verbose --no-cache",
|
||||
"test:single": "jest --verbose -w 1",
|
||||
"build": "webpack --config config/prod.webpack.config.js",
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
|||
switch (flag) {
|
||||
case 'image-builder.import.enabled':
|
||||
return true;
|
||||
case 'image-builder.new-wizard.enabled':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
@ -243,17 +241,15 @@ describe('Import model', () => {
|
|||
expect(helperText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should enable button on correct blueprint', async () => {
|
||||
test('should enable button on correct blueprint and go to wizard', async () => {
|
||||
await setUp();
|
||||
await uploadFile(`blueprints.json`, BLUEPRINT_JSON);
|
||||
const reviewButton = screen.getByTestId('import-blueprint-finish');
|
||||
await waitFor(() => expect(reviewButton).not.toHaveClass('pf-m-disabled'));
|
||||
await user.click(reviewButton);
|
||||
|
||||
await userEvent.click(reviewButton);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Image output' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Image output', { selector: 'h1' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,716 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
|
||||
import { useFlag } from '@unleash/proxy-client-react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import ImageCreator from './ImageCreator';
|
||||
import {
|
||||
awsTarget,
|
||||
fileSystemConfiguration,
|
||||
googleCloudTarget,
|
||||
imageName,
|
||||
imageOutput,
|
||||
msAzureTarget,
|
||||
packages,
|
||||
packagesContentSources,
|
||||
registration,
|
||||
repositories,
|
||||
review,
|
||||
oscap,
|
||||
} from './steps';
|
||||
import {
|
||||
fileSystemConfigurationValidator,
|
||||
targetEnvironmentValidator,
|
||||
} from './validators';
|
||||
|
||||
import './CreateImageWizard.scss';
|
||||
import {
|
||||
CDN_PROD_URL,
|
||||
CDN_STAGE_URL,
|
||||
UNIT_GIB,
|
||||
UNIT_KIB,
|
||||
UNIT_MIB,
|
||||
} from '../../constants';
|
||||
import {
|
||||
useComposeImageMutation,
|
||||
useGetComposeStatusQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import isRhel from '../../Utilities/isRhel';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useGetEnvironment } from '../../Utilities/useGetEnvironment';
|
||||
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
|
||||
|
||||
const handleKeyDown = (e, handleClose) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = (values) => {
|
||||
const customizations = {
|
||||
packages: values['selected-packages']?.map((p) => p.name),
|
||||
};
|
||||
|
||||
if (values['payload-repositories']?.length > 0) {
|
||||
customizations['payload_repositories'] = [
|
||||
...values['payload-repositories'],
|
||||
];
|
||||
}
|
||||
|
||||
if (values['custom-repositories']?.length > 0) {
|
||||
customizations['custom_repositories'] = [...values['custom-repositories']];
|
||||
}
|
||||
|
||||
if (values['register-system'] === 'register-now-rhc') {
|
||||
customizations.subscription = {
|
||||
'activation-key': values['subscription-activation-key'],
|
||||
insights: true,
|
||||
rhc: true,
|
||||
organization: Number(values['subscription-organization-id']),
|
||||
'server-url': values['subscription-server-url'],
|
||||
'base-url': values['subscription-base-url'],
|
||||
};
|
||||
} else 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': values['subscription-server-url'],
|
||||
'base-url': values['subscription-base-url'],
|
||||
};
|
||||
} 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': values['subscription-server-url'],
|
||||
'base-url': values['subscription-base-url'],
|
||||
};
|
||||
}
|
||||
|
||||
if (values['file-system-config-radio'] === 'manual') {
|
||||
customizations.filesystem = [];
|
||||
for (const fsc of values['file-system-configuration']) {
|
||||
customizations.filesystem.push({
|
||||
mountpoint: fsc.mountpoint,
|
||||
min_size: fsc.size * fsc.unit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (values['oscap-profile']) {
|
||||
customizations.openscap = {
|
||||
profile_id: values['oscap-profile'],
|
||||
};
|
||||
if (values['kernel']) {
|
||||
customizations.kernel = values['kernel'];
|
||||
}
|
||||
if (
|
||||
(Array.isArray(values['enabledServices']) &&
|
||||
values['enabledServices'].length > 0) ||
|
||||
(Array.isArray(values['maskedServices']) &&
|
||||
values['maskedServices'].length > 0)
|
||||
) {
|
||||
customizations.services = {};
|
||||
if (values['enabledServices'].length > 0) {
|
||||
customizations.services.enabled = values['enabledServices'];
|
||||
}
|
||||
if (values['maskedServices'].length > 0) {
|
||||
customizations.services.masked = values['maskedServices'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const requests = [];
|
||||
if (values['target-environment']?.aws) {
|
||||
const options =
|
||||
values['aws-target-type'] === 'aws-target-type-source'
|
||||
? { share_with_sources: [values['aws-sources-select']] }
|
||||
: { share_with_accounts: [values['aws-account-id']] };
|
||||
const request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_description: values?.['image-description'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: values['arch'],
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options,
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
client_id: 'ui',
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.gcp) {
|
||||
let share = '';
|
||||
if (values['image-sharing'] === 'gcp-account') {
|
||||
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;
|
||||
// no default
|
||||
}
|
||||
}
|
||||
|
||||
const request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_description: values?.['image-description'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: values['arch'],
|
||||
image_type: 'gcp',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
client_id: 'ui',
|
||||
};
|
||||
|
||||
if (share !== '') {
|
||||
request.image_requests[0].upload_request.options.share_with_accounts = [
|
||||
share,
|
||||
];
|
||||
}
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.azure) {
|
||||
const upload_options =
|
||||
values['azure-type'] === 'azure-type-source'
|
||||
? { source_id: values['azure-sources-select'] }
|
||||
: {
|
||||
tenant_id: values['azure-tenant-id'],
|
||||
subscription_id: values['azure-subscription-id'],
|
||||
};
|
||||
const request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_description: values?.['image-description'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: values['arch'],
|
||||
image_type: 'azure',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {
|
||||
...upload_options,
|
||||
resource_group: values['azure-resource-group'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
client_id: 'ui',
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.oci) {
|
||||
const request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_description: values?.['image-description'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: values['arch'],
|
||||
image_type: 'oci',
|
||||
upload_request: {
|
||||
type: 'oci.objectstorage',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
client_id: 'ui',
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.vsphere) {
|
||||
const request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_description: values?.['image-description'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: values['arch'],
|
||||
image_type: 'vsphere',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
client_id: 'ui',
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.['vsphere-ova']) {
|
||||
const request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_description: values?.['image-description'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: values['arch'],
|
||||
image_type: 'vsphere-ova',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
client_id: 'ui',
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.['guest-image']) {
|
||||
const request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_description: values?.['image-description'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: values['arch'],
|
||||
image_type: 'guest-image',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
client_id: 'ui',
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.['image-installer']) {
|
||||
const request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_description: values?.['image-description'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: values['arch'],
|
||||
image_type: 'image-installer',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
client_id: 'ui',
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.wsl) {
|
||||
const request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_description: values?.['image-description'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: values['arch'],
|
||||
image_type: 'wsl',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
client_id: 'ui',
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
return requests;
|
||||
};
|
||||
|
||||
const parseSizeUnit = (bytesize) => {
|
||||
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;
|
||||
}
|
||||
|
||||
return [size, unit];
|
||||
};
|
||||
|
||||
// map the compose request object to the expected form state
|
||||
const requestToState = (composeRequest, isProd, enableOscap) => {
|
||||
if (composeRequest) {
|
||||
const imageRequest = composeRequest.image_requests[0];
|
||||
const uploadRequest = imageRequest.upload_request;
|
||||
const formState = {};
|
||||
|
||||
formState['image-name'] = composeRequest.image_name;
|
||||
formState['image-description'] = composeRequest.image_description;
|
||||
|
||||
formState.release = composeRequest?.distribution;
|
||||
formState.arch = imageRequest.architecture;
|
||||
// set defaults for target environment first
|
||||
formState['target-environment'] = {
|
||||
aws: false,
|
||||
azure: false,
|
||||
gcp: false,
|
||||
oci: 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' ||
|
||||
uploadRequest.type === 'oci.objectstorage'
|
||||
) {
|
||||
targetEnvironment = imageRequest.image_type;
|
||||
} else {
|
||||
targetEnvironment = uploadRequest.type;
|
||||
}
|
||||
|
||||
formState['target-environment'][targetEnvironment] = true;
|
||||
|
||||
if (targetEnvironment === 'aws') {
|
||||
const shareWithSource = uploadRequest?.options?.share_with_sources?.[0];
|
||||
const shareWithAccount = uploadRequest?.options?.share_with_accounts?.[0];
|
||||
formState['aws-sources-select'] = shareWithSource;
|
||||
formState['aws-account-id'] = shareWithAccount;
|
||||
if (shareWithAccount && !shareWithSource) {
|
||||
formState['aws-target-type'] = 'aws-target-type-account-id';
|
||||
} else {
|
||||
// if both shareWithAccount & shareWithSource are present, set radio
|
||||
// to sources - this is essentially an arbitrary decision
|
||||
// additionally, note that the source is not validated against the actual
|
||||
// sources
|
||||
formState['aws-target-type'] = 'aws-target-type-source';
|
||||
}
|
||||
} else if (targetEnvironment === 'azure') {
|
||||
if (uploadRequest?.options?.source_id) {
|
||||
formState['azure-type'] = 'azure-type-source';
|
||||
formState['azure-sources-select'] = uploadRequest?.options?.source_id;
|
||||
} else {
|
||||
formState['azure-type'] = 'azure-type-manual';
|
||||
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;
|
||||
// no default
|
||||
}
|
||||
}
|
||||
|
||||
// customizations
|
||||
// packages
|
||||
const packages = composeRequest?.customizations?.packages?.map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
summary: undefined,
|
||||
};
|
||||
});
|
||||
formState['selected-packages'] = packages;
|
||||
|
||||
// repositories
|
||||
// 'original-payload-repositories' is treated as read-only and is used to populate
|
||||
// the table in the repositories step
|
||||
// This is necessary because there may be repositories present in the request's
|
||||
// json blob that are not managed using the content sources API. In that case,
|
||||
// they are still displayed in the table of repositories but without any information
|
||||
// from the content sources API (in other words, only the URL of the repository is
|
||||
// displayed). This information needs to persist throughout the lifetime of the
|
||||
// Wizard as it is needed every time the repositories step is visited.
|
||||
formState['original-payload-repositories'] =
|
||||
composeRequest?.customizations?.payload_repositories;
|
||||
// 'payload-repositories' is mutable and is used to generate the request
|
||||
// sent to image-builder
|
||||
formState['payload-repositories'] =
|
||||
composeRequest?.customizations?.payload_repositories;
|
||||
|
||||
// these will be overwritten by the repositories step if revisited, and generated from the payload repositories added there
|
||||
formState['custom-repositories'] =
|
||||
composeRequest?.customizations?.custom_repositories;
|
||||
|
||||
// filesystem
|
||||
const fs = composeRequest?.customizations?.filesystem;
|
||||
if (fs) {
|
||||
formState['file-system-config-radio'] = 'manual';
|
||||
const fileSystemConfiguration = [];
|
||||
for (const fsc of fs) {
|
||||
const [size, unit] = parseSizeUnit(fsc.min_size);
|
||||
fileSystemConfiguration.push({
|
||||
mountpoint: fsc.mountpoint.includes('/usr/')
|
||||
? '/usr'
|
||||
: fsc.mountpoint,
|
||||
size,
|
||||
unit,
|
||||
});
|
||||
}
|
||||
|
||||
formState['file-system-configuration'] = fileSystemConfiguration;
|
||||
}
|
||||
|
||||
// subscription
|
||||
const subscription = composeRequest?.customizations?.subscription;
|
||||
if (subscription) {
|
||||
if (subscription.rhc) {
|
||||
formState['register-system'] = 'register-now-rhc';
|
||||
} else 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;
|
||||
|
||||
if (isProd) {
|
||||
formState['subscription-server-url'] = 'subscription.rhsm.redhat.com';
|
||||
formState['subscription-base-url'] = CDN_PROD_URL;
|
||||
} else {
|
||||
formState['subscription-server-url'] =
|
||||
'subscription.rhsm.stage.redhat.com';
|
||||
formState['subscription-base-url'] = CDN_STAGE_URL;
|
||||
}
|
||||
} else {
|
||||
formState['register-system'] = 'register-later';
|
||||
}
|
||||
|
||||
if (enableOscap) {
|
||||
formState['oscap-profile'] =
|
||||
composeRequest?.customizations?.openscap?.profile_id;
|
||||
formState['kernel'] = composeRequest?.customizations?.kernel;
|
||||
formState['enabledServices'] =
|
||||
composeRequest?.customizations?.services?.enabled;
|
||||
formState['maskedServices'] =
|
||||
composeRequest?.customizations?.services?.masked;
|
||||
}
|
||||
|
||||
return formState;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const formStepHistory = (
|
||||
composeRequest,
|
||||
contentSourcesEnabled,
|
||||
enableOscap
|
||||
) => {
|
||||
if (composeRequest) {
|
||||
const imageRequest = composeRequest.image_requests[0];
|
||||
const uploadRequest = imageRequest.upload_request;
|
||||
// the order of steps must match the order of the steps in the Wizard
|
||||
const steps = ['image-output'];
|
||||
|
||||
if (uploadRequest.type === 'aws') {
|
||||
steps.push('aws-target-env');
|
||||
} else if (uploadRequest.type === 'azure') {
|
||||
steps.push('ms-azure-target-env');
|
||||
} else if (uploadRequest.type === 'gcp') {
|
||||
steps.push('google-cloud-target-env');
|
||||
}
|
||||
|
||||
if (isRhel(composeRequest?.distribution)) {
|
||||
steps.push('registration');
|
||||
}
|
||||
|
||||
if (enableOscap) {
|
||||
steps.push('Compliance');
|
||||
}
|
||||
|
||||
if (contentSourcesEnabled) {
|
||||
steps.push('File system configuration', 'packages', 'repositories');
|
||||
|
||||
const customRepositories =
|
||||
composeRequest.customizations?.payload_repositories;
|
||||
if (customRepositories) {
|
||||
steps.push('packages-content-sources');
|
||||
}
|
||||
} else {
|
||||
steps.push('File system configuration', 'packages');
|
||||
}
|
||||
steps.push('details');
|
||||
|
||||
return steps;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const CreateImageWizard = () => {
|
||||
const [composeImage] = useComposeImageMutation();
|
||||
const navigate = useNavigate();
|
||||
// composeId is an optional param that is used for Recreate image
|
||||
const { composeId } = useParams();
|
||||
|
||||
const { data } = useGetComposeStatusQuery(
|
||||
{ composeId: composeId },
|
||||
{
|
||||
skip: composeId ? false : true,
|
||||
}
|
||||
);
|
||||
const composeRequest = composeId ? data?.request : undefined;
|
||||
const contentSourcesEnabled = useFlag('image-builder.enable-content-sources');
|
||||
|
||||
// Assume that if a request is available that we should start on review step
|
||||
// This will occur if 'Recreate image' is clicked
|
||||
const initialStep = composeRequest ? 'review' : undefined;
|
||||
|
||||
const { isBeta, isProd } = useGetEnvironment();
|
||||
|
||||
// Only allow oscap to be used in Beta even if the flag says the feature is
|
||||
// activated.
|
||||
const oscapFeatureFlag = useFlag('image-builder.wizard.oscap.enabled');
|
||||
let initialState = requestToState(composeRequest, isProd(), oscapFeatureFlag);
|
||||
const stepHistory = formStepHistory(
|
||||
composeRequest,
|
||||
contentSourcesEnabled,
|
||||
oscapFeatureFlag
|
||||
);
|
||||
|
||||
if (initialState) {
|
||||
initialState.isBeta = isBeta();
|
||||
initialState.contentSourcesEnabled = contentSourcesEnabled;
|
||||
initialState.enableOscap = oscapFeatureFlag;
|
||||
} else {
|
||||
initialState = {
|
||||
isBeta: isBeta(),
|
||||
enableOscap: oscapFeatureFlag,
|
||||
contentSourcesEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
const handleClose = () => navigate(resolveRelPath(''));
|
||||
|
||||
return (
|
||||
<>
|
||||
<ImageBuilderHeader />
|
||||
<section className="pf-l-page__main-section pf-c-page__main-section">
|
||||
<ImageCreator
|
||||
onClose={handleClose}
|
||||
onSubmit={async ({ values, setIsSaving }) => {
|
||||
setIsSaving(true);
|
||||
const requests = onSave(values);
|
||||
await Promise.allSettled(
|
||||
requests.map((composeRequest) => composeImage({ composeRequest }))
|
||||
);
|
||||
navigate(resolveRelPath(''));
|
||||
}}
|
||||
defaultArch="x86_64"
|
||||
customValidatorMapper={{
|
||||
fileSystemConfigurationValidator,
|
||||
targetEnvironmentValidator,
|
||||
}}
|
||||
schema={{
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.WIZARD,
|
||||
name: 'image-builder-wizard',
|
||||
className: 'imageBuilder',
|
||||
isDynamic: true,
|
||||
inModal: false,
|
||||
onKeyDown: (e) => {
|
||||
handleKeyDown(e, handleClose);
|
||||
},
|
||||
buttonLabels: {
|
||||
submit: 'Create image',
|
||||
},
|
||||
showTitles: true,
|
||||
crossroads: [
|
||||
'target-environment',
|
||||
'release',
|
||||
'payload-repositories',
|
||||
],
|
||||
// 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,
|
||||
googleCloudTarget,
|
||||
msAzureTarget,
|
||||
registration,
|
||||
packages,
|
||||
packagesContentSources,
|
||||
repositories,
|
||||
fileSystemConfiguration,
|
||||
imageName,
|
||||
review,
|
||||
oscap,
|
||||
],
|
||||
initialState: {
|
||||
activeStep: 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}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateImageWizard;
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
.pf-v5-c-wizard__nav-list {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.pf-v5-c-wizard__nav {
|
||||
overflow-y: unset;
|
||||
}
|
||||
|
||||
.pf-c-popover[data-popper-reference-hidden="true"] {
|
||||
font-weight: initial;
|
||||
visibility: initial;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.pf-v5-c-dual-list-selector {
|
||||
--pf-v5-c-dual-list-selector__menu--MinHeight: 18rem;
|
||||
--pf-v5-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max: 100vw;
|
||||
}
|
||||
|
||||
.pf-c-form {
|
||||
--pf-c-form--GridGap: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
.pf-c-form__group-label {
|
||||
--pf-c-form__group-label--PaddingBottom: var(--pf-global--spacer--xs);
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tile {
|
||||
flex: 1 0 0px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.pf-c-tile:focus {
|
||||
--pf-c-tile__title--Color: var(--pf-c-tile__title--Color);
|
||||
--pf-c-tile__icon--Color: var(---pf-global--Color--100);
|
||||
--pf-c-tile--before--BorderWidth: var(--pf-global--BorderWidth--sm);
|
||||
--pf-c-tile--before--BorderColor: var(--pf-global--BorderColor--100);
|
||||
}
|
||||
|
||||
.pf-c-tile.pf-m-selected:focus {
|
||||
--pf-c-tile__title--Color: var(--pf-c-tile--focus__title--Color);
|
||||
--pf-c-tile__icon--Color: var(--pf-c-tile--focus__icon--Color);
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.pf-u-min-width {
|
||||
--pf-u-min-width--MinWidth: 18ch;
|
||||
}
|
||||
|
||||
.pf-u-max-width {
|
||||
--pf-u-max-width--MaxWidth: 26rem;
|
||||
}
|
||||
|
||||
ul.pf-m-plain {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { componentMapper } from '@data-driven-forms/pf4-component-mapper';
|
||||
import Pf4FormTemplate from '@data-driven-forms/pf4-component-mapper/form-template';
|
||||
import Select from '@data-driven-forms/pf4-component-mapper/select';
|
||||
import FormRenderer from '@data-driven-forms/react-form-renderer/form-renderer';
|
||||
import { Spinner } from '@patternfly/react-core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ActivationKeys from './formComponents/ActivationKeys';
|
||||
import ArchSelect from './formComponents/ArchSelect';
|
||||
import { AWSSourcesSelect } from './formComponents/AWSSourcesSelect';
|
||||
import AzureAuthButton from './formComponents/AzureAuthButton';
|
||||
import AzureResourceGroups from './formComponents/AzureResourceGroups';
|
||||
import AzureSourcesSelect from './formComponents/AzureSourcesSelect';
|
||||
import CentOSAcknowledgement from './formComponents/CentOSAcknowledgement';
|
||||
import FileSystemConfiguration from './formComponents/FileSystemConfiguration';
|
||||
import GalleryLayout from './formComponents/GalleryLayout';
|
||||
import ImageOutputReleaseSelect from './formComponents/ImageOutputReleaseSelect';
|
||||
import { Oscap } from './formComponents/Oscap';
|
||||
import {
|
||||
ContentSourcesPackages,
|
||||
RedHatPackages,
|
||||
} from './formComponents/Packages';
|
||||
import RadioWithPopover from './formComponents/RadioWithPopover';
|
||||
import Registration from './formComponents/Registration';
|
||||
import RegistrationKeyInformation from './formComponents/RegistrationKeyInformation';
|
||||
import ReleaseLifecycle from './formComponents/ReleaseLifecycle';
|
||||
import Repositories from './formComponents/Repositories';
|
||||
import Review from './formComponents/ReviewStep';
|
||||
import TargetEnvironment from './formComponents/TargetEnvironment';
|
||||
|
||||
const ImageCreator = ({
|
||||
schema,
|
||||
onSubmit,
|
||||
onClose,
|
||||
customComponentMapper,
|
||||
customValidatorMapper,
|
||||
defaultArch,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return schema ? (
|
||||
<FormRenderer
|
||||
initialValues={props.initialValues}
|
||||
schema={schema}
|
||||
className={`image-builder${className ? ` ${className}` : ''}`}
|
||||
subscription={{ values: true }}
|
||||
FormTemplate={(props) => (
|
||||
<Pf4FormTemplate {...props} showFormControls={false} />
|
||||
)}
|
||||
onSubmit={(formValues) => onSubmit(formValues)}
|
||||
validatorMapper={{ ...customValidatorMapper }}
|
||||
componentMapper={{
|
||||
...componentMapper,
|
||||
review: Review,
|
||||
output: TargetEnvironment,
|
||||
select: Select,
|
||||
'package-selector': {
|
||||
component: RedHatPackages,
|
||||
defaultArch,
|
||||
},
|
||||
'package-selector-content-sources': {
|
||||
component: ContentSourcesPackages,
|
||||
},
|
||||
'radio-popover': RadioWithPopover,
|
||||
'azure-auth-button': AzureAuthButton,
|
||||
'activation-keys': ActivationKeys,
|
||||
'activation-key-information': RegistrationKeyInformation,
|
||||
'file-system-configuration': FileSystemConfiguration,
|
||||
'image-output-release-select': ImageOutputReleaseSelect,
|
||||
'centos-acknowledgement': CentOSAcknowledgement,
|
||||
'repositories-table': Repositories,
|
||||
'aws-sources-select': AWSSourcesSelect,
|
||||
'azure-sources-select': AzureSourcesSelect,
|
||||
'azure-resource-groups': AzureResourceGroups,
|
||||
'gallery-layout': GalleryLayout,
|
||||
'oscap-profile-selector': Oscap,
|
||||
'image-output-arch-select': ArchSelect,
|
||||
'image-output-release-lifecycle': ReleaseLifecycle,
|
||||
registration: Registration,
|
||||
...customComponentMapper,
|
||||
}}
|
||||
onCancel={onClose}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<Spinner />
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
export default ImageCreator;
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { FormSpy } from '@data-driven-forms/react-form-renderer';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import { Alert } from '@patternfly/react-core';
|
||||
import { FormGroup, Spinner } from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { extractProvisioningList } from '../../../store/helpers';
|
||||
import {
|
||||
useGetSourceListQuery,
|
||||
useGetSourceUploadInfoQuery,
|
||||
} from '../../../store/provisioningApi';
|
||||
|
||||
export const AWSSourcesSelect = ({
|
||||
label,
|
||||
isRequired,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedSourceId, setSelectedSourceId] = useState(
|
||||
getState()?.values?.['aws-sources-select']
|
||||
);
|
||||
|
||||
const {
|
||||
data: rawSources,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
refetch,
|
||||
} = useGetSourceListQuery({ provider: 'aws' });
|
||||
const sources = extractProvisioningList(rawSources);
|
||||
|
||||
const {
|
||||
data: sourceDetails,
|
||||
isFetching: isFetchingDetails,
|
||||
isSuccess: isSuccessDetails,
|
||||
isError: isErrorDetails,
|
||||
} = useGetSourceUploadInfoQuery(
|
||||
{ id: selectedSourceId },
|
||||
{
|
||||
skip: !selectedSourceId,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchingDetails || !isSuccessDetails) return;
|
||||
change('aws-associated-account-id', sourceDetails?.aws?.account_id);
|
||||
}, [
|
||||
isFetchingDetails,
|
||||
isSuccessDetails,
|
||||
change,
|
||||
sourceDetails?.aws?.account_id,
|
||||
]);
|
||||
|
||||
const onFormChange = ({ values }) => {
|
||||
if (
|
||||
values['aws-target-type'] !== 'aws-target-type-source' ||
|
||||
values[input.name] === undefined
|
||||
) {
|
||||
change(input.name, undefined);
|
||||
change('aws-associated-account-id', undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (_, sourceName) => {
|
||||
const sourceId = sources.find((source) => source.name === sourceName).id;
|
||||
setSelectedSourceId(sourceId);
|
||||
setIsOpen(false);
|
||||
change(input.name, sourceId);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setSelectedSourceId();
|
||||
change(input.name, undefined);
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
// Refetch upon opening (but not upon closing)
|
||||
if (!isOpen) {
|
||||
refetch();
|
||||
}
|
||||
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormSpy subscription={{ values: true }} onChange={onFormChange} />
|
||||
<FormGroup
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
data-testid="sources"
|
||||
className={className}
|
||||
>
|
||||
<Select
|
||||
ouiaId="source_select"
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={handleToggle}
|
||||
onSelect={handleSelect}
|
||||
onClear={handleClear}
|
||||
selections={
|
||||
selectedSourceId
|
||||
? sources.find((source) => source.id === selectedSourceId)?.name
|
||||
: undefined
|
||||
}
|
||||
isOpen={isOpen}
|
||||
placeholderText="Select source"
|
||||
typeAheadAriaLabel="Select source"
|
||||
isDisabled={!isSuccess}
|
||||
>
|
||||
{isSuccess &&
|
||||
sources.map((source) => (
|
||||
<SelectOption key={source.id} value={source.name} />
|
||||
))}
|
||||
{isFetching && (
|
||||
<SelectOption isNoResultsOption={true}>
|
||||
<Spinner size="lg" />
|
||||
</SelectOption>
|
||||
)}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
<>
|
||||
{isError && (
|
||||
<Alert
|
||||
variant={'danger'}
|
||||
isPlain={true}
|
||||
isInline={true}
|
||||
title={'Sources unavailable'}
|
||||
>
|
||||
Sources cannot be reached, try again later or enter an AWS account
|
||||
ID manually.
|
||||
</Alert>
|
||||
)}
|
||||
{!isError && isErrorDetails && (
|
||||
<Alert
|
||||
variant={'danger'}
|
||||
isPlain
|
||||
isInline
|
||||
title={'AWS details unavailable'}
|
||||
>
|
||||
The AWS account ID for the selected source could not be resolved.
|
||||
There might be a problem with the source. Verify that the source is
|
||||
valid in Sources or select a different source.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AWSSourcesSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
|
||||
import { useFormApi } from '@data-driven-forms/react-form-renderer';
|
||||
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
|
||||
import {
|
||||
Alert,
|
||||
Spinner,
|
||||
Text,
|
||||
TextContent,
|
||||
TextList,
|
||||
TextListItem,
|
||||
TextListItemVariants,
|
||||
TextListVariants,
|
||||
TextVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { Button, Popover } from '@patternfly/react-core';
|
||||
import { HelpIcon } from '@patternfly/react-icons';
|
||||
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
||||
|
||||
import { useShowActivationKeyQuery } from '../../../store/rhsmApi';
|
||||
|
||||
const ActivationKeyInformation = (): JSX.Element => {
|
||||
const { getState } = useFormApi();
|
||||
const { currentStep } = useContext(WizardContext);
|
||||
|
||||
const activationKey = getState()?.values?.['subscription-activation-key'];
|
||||
|
||||
const {
|
||||
data: activationKeyInfo,
|
||||
isFetching: isFetchingActivationKeyInfo,
|
||||
isSuccess: isSuccessActivationKeyInfo,
|
||||
isError: isErrorActivationKeyInfo,
|
||||
} = useShowActivationKeyQuery(
|
||||
{ name: activationKey },
|
||||
{
|
||||
skip: !activationKey,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFetchingActivationKeyInfo && <Spinner size="lg" />}
|
||||
{isSuccessActivationKeyInfo && (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Name:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{activationKey}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Role:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{activationKeyInfo.body?.role || 'Not defined'}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
SLA:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{activationKeyInfo.body?.serviceLevel || 'Not defined'}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Usage:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{activationKeyInfo.body?.usage || 'Not defined'}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Additional repositories:
|
||||
<Popover
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
The core repositories for your operating system version
|
||||
are always enabled and do not need to be explicitly added
|
||||
to the activation key.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="About additional repositories"
|
||||
className="pf-u-pl-sm pf-u-pt-0 pf-u-pb-0"
|
||||
size="sm"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dd}
|
||||
className="pf-u-display-flex pf-u-align-items-flex-end"
|
||||
>
|
||||
{activationKeyInfo.body?.additionalRepositories &&
|
||||
activationKeyInfo.body?.additionalRepositories?.length > 0 ? (
|
||||
<Popover
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>
|
||||
Additional repositories
|
||||
</Text>
|
||||
<Table
|
||||
aria-label="Additional repositories table"
|
||||
variant="compact"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody data-testid="additional-repositories-table">
|
||||
{activationKeyInfo.body?.additionalRepositories?.map(
|
||||
(repo, index) => (
|
||||
<Tr key={index}>
|
||||
<Td>{repo.repositoryLabel}</Td>
|
||||
</Tr>
|
||||
)
|
||||
)}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
data-testid="repositories-popover-button"
|
||||
variant="link"
|
||||
aria-label="Show additional repositories"
|
||||
className="pf-u-pl-0 pf-u-pt-0 pf-u-pb-0"
|
||||
>
|
||||
{activationKeyInfo.body?.additionalRepositories?.length}{' '}
|
||||
repositories
|
||||
</Button>
|
||||
</Popover>
|
||||
) : (
|
||||
'None'
|
||||
)}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
)}
|
||||
{isErrorActivationKeyInfo && (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Name:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{activationKey}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
)}
|
||||
{isErrorActivationKeyInfo && currentStep.name === 'registration' && (
|
||||
<>
|
||||
<br />
|
||||
<Alert
|
||||
title="Information about the activation key unavailable"
|
||||
variant="danger"
|
||||
isPlain
|
||||
isInline
|
||||
>
|
||||
Information about the activation key cannot be loaded. Please check
|
||||
the key was not removed and try again later.
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivationKeyInformation;
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import {
|
||||
Alert,
|
||||
FormGroup,
|
||||
Spinner,
|
||||
EmptyState,
|
||||
Button,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody,
|
||||
EmptyStateHeader,
|
||||
EmptyStateFooter,
|
||||
EmptyStateActions,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import { WrenchIcon, AddCircleOIcon } from '@patternfly/react-icons';
|
||||
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { CDN_PROD_URL, CDN_STAGE_URL } from '../../../constants';
|
||||
import {
|
||||
useListActivationKeysQuery,
|
||||
useCreateActivationKeysMutation,
|
||||
} from '../../../store/rhsmApi';
|
||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
||||
|
||||
const EmptyActivationsKeyState = ({ handleActivationKeyFn, isLoading }) => (
|
||||
<EmptyState variant="xs">
|
||||
<EmptyStateHeader
|
||||
titleText="No activation keys found"
|
||||
headingLevel="h4"
|
||||
icon={<EmptyStateIcon icon={WrenchIcon} />}
|
||||
/>
|
||||
<EmptyStateBody>
|
||||
Get started by building a default key, which will be generated and present
|
||||
for you.
|
||||
</EmptyStateBody>
|
||||
<EmptyStateFooter>
|
||||
<EmptyStateActions>
|
||||
<Button
|
||||
onClick={handleActivationKeyFn}
|
||||
icon={<AddCircleOIcon />}
|
||||
isLoading={isLoading}
|
||||
iconPosition="left"
|
||||
variant="link"
|
||||
>
|
||||
Create activation key
|
||||
</Button>
|
||||
</EmptyStateActions>
|
||||
</EmptyStateFooter>
|
||||
</EmptyState>
|
||||
);
|
||||
|
||||
EmptyActivationsKeyState.propTypes = {
|
||||
handleActivationKeyFn: PropTypes.func.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
const ActivationKeys = ({ label, isRequired, ...props }) => {
|
||||
const { isProd } = useGetEnvironment();
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [activationKeySelected, selectActivationKey] = useState(
|
||||
getState()?.values?.['subscription-activation-key']
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
data: activationKeys,
|
||||
isFetching: isFetchingActivationKeys,
|
||||
isSuccess: isSuccessActivationKeys,
|
||||
isError: isErrorActivationKeys,
|
||||
refetch,
|
||||
} = useListActivationKeysQuery();
|
||||
|
||||
const [createActivationKey, { isLoading: isLoadingActivationKey }] =
|
||||
useCreateActivationKeysMutation();
|
||||
useEffect(() => {
|
||||
if (isProd()) {
|
||||
change('subscription-server-url', 'subscription.rhsm.redhat.com');
|
||||
change('subscription-base-url', CDN_PROD_URL);
|
||||
} else {
|
||||
change('subscription-server-url', 'subscription.rhsm.stage.redhat.com');
|
||||
change('subscription-base-url', CDN_STAGE_URL);
|
||||
}
|
||||
}, [isProd, change]);
|
||||
|
||||
const setActivationKey = (_, selection) => {
|
||||
selectActivationKey(selection);
|
||||
setIsOpen(false);
|
||||
change(input.name, selection);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
selectActivationKey();
|
||||
change(input.name, undefined);
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!isOpen) {
|
||||
refetch();
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleCreateActivationKey = async () => {
|
||||
const res = await createActivationKey({
|
||||
body: {
|
||||
name: 'activation-key-default',
|
||||
serviceLevel: 'Self-Support',
|
||||
},
|
||||
});
|
||||
refetch();
|
||||
if (res.error) {
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'danger',
|
||||
title: 'Error creating activation key',
|
||||
description: res.error?.data?.error?.message,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const isActivationKeysEmpty =
|
||||
isSuccessActivationKeys && activationKeys.body.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
data-testid="subscription-activation-key"
|
||||
>
|
||||
<Select
|
||||
ouiaId="activation_key_select"
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={handleToggle}
|
||||
onSelect={setActivationKey}
|
||||
onClear={handleClear}
|
||||
selections={activationKeySelected}
|
||||
isOpen={isOpen}
|
||||
placeholderText="Select activation key"
|
||||
typeAheadAriaLabel="Select activation key"
|
||||
isDisabled={!isSuccessActivationKeys}
|
||||
>
|
||||
{isActivationKeysEmpty && (
|
||||
<EmptyActivationsKeyState
|
||||
handleActivationKeyFn={handleCreateActivationKey}
|
||||
isLoading={isLoadingActivationKey}
|
||||
/>
|
||||
)}
|
||||
{isSuccessActivationKeys &&
|
||||
activationKeys.body.map((key, index) => (
|
||||
<SelectOption key={index} value={key.name} />
|
||||
))}
|
||||
{!isSuccessActivationKeys && isFetchingActivationKeys && (
|
||||
<SelectOption
|
||||
isNoResultsOption={true}
|
||||
data-testid="activation-keys-loading"
|
||||
>
|
||||
<Spinner size="md" />
|
||||
</SelectOption>
|
||||
)}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
{isErrorActivationKeys && (
|
||||
<Alert
|
||||
title="Activation keys unavailable"
|
||||
variant="danger"
|
||||
isPlain
|
||||
isInline
|
||||
>
|
||||
Activation keys cannot be reached, try again later.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ActivationKeys.propTypes = {
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
||||
ActivationKeys.defaultProps = {
|
||||
label: '',
|
||||
isRequired: false,
|
||||
};
|
||||
|
||||
export default ActivationKeys;
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { FormSpy } from '@data-driven-forms/react-form-renderer';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { ARCHS, AARCH64 } from '../../../constants';
|
||||
|
||||
const ArchSelect = ({ label, isRequired, ...props }) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Set the architecture via search parameter
|
||||
// Used by Insights assistant or external hyperlinks (access.redhat.com, developers.redhat.com)
|
||||
const preloadArch = searchParams.get('arch');
|
||||
useEffect(() => {
|
||||
preloadArch === AARCH64 && change(input.name, AARCH64);
|
||||
}, [change, input.name, preloadArch]);
|
||||
|
||||
const setArch = (_, selection) => {
|
||||
change(input.name, selection);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const setSelectOptions = () => {
|
||||
var options = [];
|
||||
ARCHS.forEach((arch) => {
|
||||
options.push(
|
||||
<SelectOption key={arch} value={arch}>
|
||||
{arch}
|
||||
</SelectOption>
|
||||
);
|
||||
});
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
return (
|
||||
<FormSpy>
|
||||
{() => (
|
||||
<FormGroup isRequired={isRequired} label={label}>
|
||||
<Select
|
||||
ouiaId="arch_select"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setIsOpen(!isOpen)}
|
||||
onSelect={setArch}
|
||||
selections={getState()?.values?.[input.name]}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{setSelectOptions()}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FormSpy>
|
||||
);
|
||||
};
|
||||
|
||||
ArchSelect.propTypes = {
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ArchSelect;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useGetSourceUploadInfoQuery } from '../../../store/provisioningApi';
|
||||
|
||||
type AwsAccountIdProps = {
|
||||
sourceId: number;
|
||||
};
|
||||
|
||||
export const AwsAccountId = ({ sourceId }: AwsAccountIdProps) => {
|
||||
const { data } = useGetSourceUploadInfoQuery({ id: sourceId });
|
||||
return <>{data?.aws?.account_id}</>;
|
||||
};
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import { Button, FormGroup } from '@patternfly/react-core';
|
||||
|
||||
const AzureAuthButton = () => {
|
||||
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'
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="secondary"
|
||||
isDisabled={!guidRegex.test(tenantId)}
|
||||
href={
|
||||
'https://login.microsoftonline.com/' +
|
||||
tenantId +
|
||||
'/oauth2/v2.0/authorize?client_id=b94bb246-b02c-4985-9c22-d44e66f657f4&scope=openid&' +
|
||||
'response_type=code&response_mode=query&redirect_uri=https://portal.azure.com'
|
||||
}
|
||||
>
|
||||
Authorize Image Builder
|
||||
</Button>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default AzureAuthButton;
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import FormSpy from '@data-driven-forms/react-form-renderer/form-spy';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import { FormGroup, Spinner } from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useGetSourceUploadInfoQuery } from '../../../store/provisioningApi';
|
||||
|
||||
const AzureResourceGroups = ({ label, isRequired, className, ...props }) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [sourceId, setSourceId] = useState(
|
||||
getState()?.values?.['azure-sources-select']
|
||||
);
|
||||
const onFormChange = ({ values }) => {
|
||||
setSourceId(values['azure-sources-select']);
|
||||
};
|
||||
|
||||
const { data: sourceDetails, isFetching } = useGetSourceUploadInfoQuery(
|
||||
{ id: sourceId },
|
||||
{
|
||||
skip: !sourceId,
|
||||
}
|
||||
);
|
||||
const resourceGroups =
|
||||
(sourceId && sourceDetails?.azure?.resource_groups) || [];
|
||||
|
||||
const setResourceGroup = (_, selection) => {
|
||||
setIsOpen(false);
|
||||
change(input.name, selection);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
change(input.name, undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
data-testid="azure-resource-groups"
|
||||
>
|
||||
<FormSpy subscription={{ values: true }} onChange={onFormChange} />
|
||||
<Select
|
||||
ouiaId="resource_group_select"
|
||||
variant={SelectVariant.typeahead}
|
||||
className={className}
|
||||
onToggle={() => setIsOpen(!isOpen)}
|
||||
onSelect={setResourceGroup}
|
||||
onClear={handleClear}
|
||||
selections={input.value}
|
||||
isOpen={isOpen}
|
||||
placeholderText="Select resource group"
|
||||
typeAheadAriaLabel="Select resource group"
|
||||
>
|
||||
{isFetching && (
|
||||
<SelectOption
|
||||
isNoResultsOption={true}
|
||||
data-testid="azure-resource-groups-loading"
|
||||
>
|
||||
<Spinner size="lg" />
|
||||
</SelectOption>
|
||||
)}
|
||||
{resourceGroups.map((name, index) => (
|
||||
<SelectOption
|
||||
key={index}
|
||||
value={name}
|
||||
aria-label={`Resource group ${name}`}
|
||||
/>
|
||||
))}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
AzureResourceGroups.propTypes = {
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
AzureResourceGroups.defaultProps = {
|
||||
label: '',
|
||||
isRequired: false,
|
||||
className: '',
|
||||
};
|
||||
|
||||
export default AzureResourceGroups;
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import FormSpy from '@data-driven-forms/react-form-renderer/form-spy';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import { Alert } from '@patternfly/react-core';
|
||||
import { FormGroup, Spinner } from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { extractProvisioningList } from '../../../store/helpers';
|
||||
import {
|
||||
useGetSourceListQuery,
|
||||
useGetSourceUploadInfoQuery,
|
||||
} from '../../../store/provisioningApi';
|
||||
|
||||
const AzureSourcesSelect = ({ label, isRequired, className, ...props }) => {
|
||||
const { change } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const selectedSourceId = input.value;
|
||||
|
||||
const {
|
||||
data: rawSources,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
refetch,
|
||||
} = useGetSourceListQuery({ provider: 'azure' });
|
||||
const sources = extractProvisioningList(rawSources);
|
||||
|
||||
const {
|
||||
data: sourceDetails,
|
||||
isFetching: isFetchingDetails,
|
||||
isSuccess: isSuccessDetails,
|
||||
isError: isErrorDetails,
|
||||
} = useGetSourceUploadInfoQuery(
|
||||
{ id: selectedSourceId },
|
||||
{
|
||||
skip: !selectedSourceId,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchingDetails || !isSuccessDetails) return;
|
||||
change('azure-tenant-id', sourceDetails?.azure?.tenant_id);
|
||||
change('azure-subscription-id', sourceDetails?.azure?.subscription_id);
|
||||
}, [
|
||||
isFetchingDetails,
|
||||
isSuccessDetails,
|
||||
sourceDetails?.azure?.subscription_id,
|
||||
sourceDetails?.azure?.tenant_id,
|
||||
change,
|
||||
]);
|
||||
|
||||
const onFormChange = ({ values }) => {
|
||||
if (
|
||||
values['azure-type'] !== 'azure-type-source' ||
|
||||
values[input.name] === undefined
|
||||
) {
|
||||
change(input.name, undefined);
|
||||
change('azure-tenant-id', undefined);
|
||||
change('azure-subscription-id', undefined);
|
||||
change('azure-resource-group', undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (_, sourceName) => {
|
||||
const sourceId = sources.find((source) => source.name === sourceName).id;
|
||||
change(input.name, sourceId);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
change(input.name, undefined);
|
||||
change('azure-resource-group', undefined);
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
// Refetch upon opening (but not upon closing)
|
||||
if (!isOpen) {
|
||||
refetch();
|
||||
}
|
||||
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormSpy subscription={{ values: true }} onChange={onFormChange} />
|
||||
<FormGroup
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
data-testid="azure-sources"
|
||||
>
|
||||
<Select
|
||||
ouiaId="source_select"
|
||||
variant={SelectVariant.typeahead}
|
||||
className={className}
|
||||
onToggle={handleToggle}
|
||||
onSelect={handleSelect}
|
||||
onClear={handleClear}
|
||||
selections={
|
||||
selectedSourceId
|
||||
? sources.find((source) => source.id === selectedSourceId)?.name
|
||||
: undefined
|
||||
}
|
||||
isOpen={isOpen}
|
||||
placeholderText="Select source"
|
||||
typeAheadAriaLabel="Select source"
|
||||
menuAppendTo="parent"
|
||||
maxHeight="25rem"
|
||||
isDisabled={!isSuccess}
|
||||
>
|
||||
{isSuccess &&
|
||||
sources.map((source) => (
|
||||
<SelectOption key={source.id} value={source.name} />
|
||||
))}
|
||||
{isFetching && (
|
||||
<SelectOption isNoResultsOption={true}>
|
||||
<Spinner size="lg" />
|
||||
</SelectOption>
|
||||
)}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
<>
|
||||
{isError && (
|
||||
<Alert
|
||||
variant={'danger'}
|
||||
isPlain
|
||||
isInline
|
||||
title={'Sources unavailable'}
|
||||
>
|
||||
Sources cannot be reached, try again later or enter an account info
|
||||
for upload manually.
|
||||
</Alert>
|
||||
)}
|
||||
{!isError && isErrorDetails && (
|
||||
<Alert
|
||||
variant={'danger'}
|
||||
isPlain
|
||||
isInline
|
||||
title={'Azure details unavailable'}
|
||||
>
|
||||
Could not fetch Tenant ID and Subscription ID from Azure for given
|
||||
Source. Check Sources page for the source availability or select a
|
||||
different Source.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AzureSourcesSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AzureSourcesSelect;
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert, Button } from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { DEVELOPERS_URL } from '../../../constants';
|
||||
|
||||
const DeveloperProgramButton = () => {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={DEVELOPERS_URL}
|
||||
>
|
||||
Red Hat Developer Program
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const CentOSAcknowledgement = () => {
|
||||
return (
|
||||
<Alert
|
||||
variant="info"
|
||||
isPlain
|
||||
isInline
|
||||
title={
|
||||
<>
|
||||
CentOS Stream builds are intended for the development of future
|
||||
versions of RHEL and are not supported for production workloads or
|
||||
other use cases.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Join the <DeveloperProgramButton /> to learn about paid and no-cost RHEL
|
||||
subscription options.
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default CentOSAcknowledgement;
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
|
||||
import { FormSpy, useFormApi } from '@data-driven-forms/react-form-renderer';
|
||||
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { contentSourcesApi } from '../../../store/contentSourcesApi';
|
||||
import { rhsmApi } from '../../../store/rhsmApi';
|
||||
import { useCheckRepositoriesAvailability } from '../../../Utilities/checkRepositoriesAvailability';
|
||||
import { releaseToVersion } from '../../../Utilities/releaseToVersion';
|
||||
|
||||
const CustomButtons = ({
|
||||
buttonLabels: { cancel, next, submit, back },
|
||||
handleNext,
|
||||
handlePrev,
|
||||
nextStep,
|
||||
}) => {
|
||||
const { getState } = useFormApi();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const { currentStep, formOptions } = useContext(WizardContext);
|
||||
const prefetchActivationKeys = rhsmApi.usePrefetch('listActivationKeys');
|
||||
const prefetchRepositories =
|
||||
contentSourcesApi.usePrefetch('listRepositories');
|
||||
const hasUnavailableRepo = useCheckRepositoriesAvailability();
|
||||
|
||||
const onNextOrSubmit = () => {
|
||||
if (currentStep.id === 'wizard-review') {
|
||||
formOptions.onSubmit({
|
||||
values: formOptions.getState().values,
|
||||
setIsSaving,
|
||||
});
|
||||
} else {
|
||||
if (typeof nextStep === 'function') {
|
||||
handleNext(nextStep({ values: formOptions.getState().values }));
|
||||
} else {
|
||||
handleNext(nextStep);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (currentStep.id === 'wizard-imageoutput') {
|
||||
prefetchActivationKeys();
|
||||
}
|
||||
if (currentStep.id === 'wizard-systemconfiguration-packages') {
|
||||
const arch = getState().values?.arch;
|
||||
const release = getState().values?.release;
|
||||
const version = releaseToVersion(release);
|
||||
prefetchRepositories({
|
||||
availableForArch: arch,
|
||||
availableForVersion: version,
|
||||
contentType: 'rpm',
|
||||
origin: 'external',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormSpy>
|
||||
{() => (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
id={`${currentStep.id}-next-button`}
|
||||
variant="primary"
|
||||
type="button"
|
||||
isDisabled={
|
||||
!formOptions.valid ||
|
||||
formOptions.getState().validating ||
|
||||
isSaving ||
|
||||
hasUnavailableRepo
|
||||
}
|
||||
isLoading={currentStep.id === 'wizard-review' ? isSaving : null}
|
||||
onClick={onNextOrSubmit}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
{currentStep.id === 'wizard-review'
|
||||
? isSaving
|
||||
? 'Creating image'
|
||||
: submit
|
||||
: next}
|
||||
</Button>
|
||||
<Button
|
||||
id={`${currentStep.id}-previous-button`}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handlePrev}
|
||||
isDisabled={isSaving}
|
||||
>
|
||||
{back}
|
||||
</Button>
|
||||
<div className="pf-c-wizard__footer-cancel">
|
||||
<Button
|
||||
id={`${currentStep.id}-cancel-button`}
|
||||
type="button"
|
||||
variant="link"
|
||||
onClick={formOptions.onCancel}
|
||||
isDisabled={isSaving}
|
||||
>
|
||||
{cancel}
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</FormSpy>
|
||||
);
|
||||
};
|
||||
|
||||
CustomButtons.propTypes = {
|
||||
buttonLabels: PropTypes.shape({
|
||||
cancel: PropTypes.node,
|
||||
submit: PropTypes.node,
|
||||
back: PropTypes.node,
|
||||
next: PropTypes.node,
|
||||
}),
|
||||
handleNext: PropTypes.func,
|
||||
handlePrev: PropTypes.func,
|
||||
nextStep: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
};
|
||||
|
||||
export default CustomButtons;
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { useFormApi } from '@data-driven-forms/react-form-renderer';
|
||||
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { imageBuilderApi } from '../../../store/imageBuilderApi';
|
||||
|
||||
// FileSystemconfigButtons are defined separately to display errors inside of the button footer
|
||||
const FileSystemConfigButtons = ({ handleNext, handlePrev, nextStep }) => {
|
||||
const { currentStep, formOptions } = useContext(WizardContext);
|
||||
const { change, getState } = useFormApi();
|
||||
const [hasErrors, setHasErrors] = useState(
|
||||
getState()?.errors?.['file-system-configuration'] ? true : false
|
||||
);
|
||||
const [nextHasBeenClicked, setNextHasBeenClicked] = useState(false);
|
||||
const prefetchArchitectures = imageBuilderApi.usePrefetch('getArchitectures');
|
||||
const errors = getState()?.errors?.['file-system-configuration'];
|
||||
|
||||
useEffect(() => {
|
||||
errors ? setHasErrors(true) : setHasErrors(false);
|
||||
|
||||
if (!errors) {
|
||||
setNextHasBeenClicked(false);
|
||||
change('file-system-config-show-errors', false);
|
||||
}
|
||||
}, [errors, change]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!hasErrors) {
|
||||
handleNext(nextStep);
|
||||
}
|
||||
|
||||
setNextHasBeenClicked(true);
|
||||
change('file-system-config-show-errors', true);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
const distribution = getState()?.values?.release;
|
||||
prefetchArchitectures({ distribution });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
id={`${currentStep.id}-next-button`}
|
||||
variant="primary"
|
||||
type="button"
|
||||
isDisabled={hasErrors && nextHasBeenClicked}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
id={`${currentStep.id}-previous-button`}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<div className="pf-c-wizard__footer-cancel">
|
||||
<Button
|
||||
id={`${currentStep.id}-cancel-button`}
|
||||
type="button"
|
||||
variant="link"
|
||||
onClick={formOptions.onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FileSystemConfigButtons.propTypes = {
|
||||
handleNext: PropTypes.func,
|
||||
handlePrev: PropTypes.func,
|
||||
nextStep: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FileSystemConfigButtons;
|
||||
|
|
@ -1,495 +0,0 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { FormSpy } from '@data-driven-forms/react-form-renderer';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Popover,
|
||||
Spinner,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
HelpIcon,
|
||||
MinusCircleIcon,
|
||||
PlusCircleIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
import styles from '@patternfly/react-styles/css/components/Table/table';
|
||||
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import MountPoint, { MountPointValidPrefixes } from './MountPoint';
|
||||
import SizeUnit from './SizeUnit';
|
||||
import UsrSubDirectoriesDisabled from './UsrSubDirectoriesDisabled';
|
||||
|
||||
import {
|
||||
FILE_SYSTEM_CUSTOMIZATION_URL,
|
||||
UNIT_GIB,
|
||||
UNIT_MIB,
|
||||
} from '../../../constants';
|
||||
import { useGetOscapCustomizationsQuery } from '../../../store/imageBuilderApi';
|
||||
|
||||
const initialRow = {
|
||||
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 oscapProfile = getState()?.values?.['oscap-profile'];
|
||||
const hasNoOscapProfile = !oscapProfile;
|
||||
const hasCustomizations = !(
|
||||
getState()?.values?.['file-system-configuration'] === undefined ||
|
||||
getState().values['file-system-configuration'].length === 1
|
||||
);
|
||||
|
||||
const {
|
||||
data: customizations,
|
||||
isFetching: isFetchingCustomizations,
|
||||
isSuccess,
|
||||
} = useGetOscapCustomizationsQuery(
|
||||
{
|
||||
distribution: getState()?.values?.['release'],
|
||||
profile: oscapProfile,
|
||||
},
|
||||
{
|
||||
// Don't override the user's data if they made customizations
|
||||
skip: hasNoOscapProfile || hasCustomizations,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCustomizations || rows.length > 1) {
|
||||
return;
|
||||
}
|
||||
if (customizations && customizations.filesystem && isSuccess) {
|
||||
const newRows = rows;
|
||||
|
||||
// The filesystem customizations required by the OpenSCAP profile may
|
||||
// contain some unsupported mountpoints. They need to be filtered out to
|
||||
// prevent undefined behaviour in the frontend and the backend
|
||||
const fss = customizations.filesystem.filter((fs) =>
|
||||
MountPointValidPrefixes.includes(
|
||||
'/'.concat(fs.mountpoint.split('/')[1])
|
||||
)
|
||||
);
|
||||
|
||||
// And add them all to the list.
|
||||
for (const fs of fss) {
|
||||
newRows.push({
|
||||
id: uuidv4(),
|
||||
mountpoint: fs.mountpoint,
|
||||
fstype: 'xfs',
|
||||
size: fs.min_size / UNIT_MIB, // the unit from the customizations are in bytes
|
||||
unit: UNIT_MIB, // and using MIB seems sensible here instead
|
||||
});
|
||||
}
|
||||
setRows(newRows);
|
||||
setItemOrder(newRows.map((row) => row.id));
|
||||
change('file-system-config-radio', 'manual');
|
||||
}
|
||||
}, [customizations, isSuccess, change, hasCustomizations, rows]);
|
||||
|
||||
useEffect(() => {
|
||||
const fsc = getState()?.values?.['file-system-configuration'];
|
||||
if (!fsc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRows = [];
|
||||
const newOrder = [];
|
||||
fsc.forEach((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);
|
||||
}, [getState]);
|
||||
|
||||
const showErrors = () =>
|
||||
getState()?.values?.['file-system-config-show-errors'];
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
}, [rows, itemOrder, change, input.name]);
|
||||
|
||||
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) => {
|
||||
const removeIndex = rows.map((e) => e.id).indexOf(id);
|
||||
const newRows = [...rows];
|
||||
newRows.splice(removeIndex, 1);
|
||||
|
||||
const removeOrderIndex = itemOrder.indexOf(id);
|
||||
const 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) => {
|
||||
const newRows = [...rows];
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
if (newRows[i].id === id) {
|
||||
const newRow = { ...newRows[i] };
|
||||
newRow.mountpoint = mp;
|
||||
newRows.splice(i, 1, newRow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setRows(newRows);
|
||||
};
|
||||
|
||||
const setSize = (id, s, u) => {
|
||||
const newRows = [...rows];
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
if (newRows[i].id === id) {
|
||||
const newRow = { ...newRows[i] };
|
||||
newRow.size = s;
|
||||
newRow.unit = u;
|
||||
newRows.splice(i, 1, newRow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setRows(newRows);
|
||||
};
|
||||
|
||||
// Don't let the user interact with the partitions while we are getting the
|
||||
// customizations. Having a customizations added by the user first would mess
|
||||
// up the logic.
|
||||
if (isFetchingCustomizations) {
|
||||
return <Spinner size="lg" />;
|
||||
}
|
||||
|
||||
const hasIsoTarget = () => {
|
||||
const isoTarget =
|
||||
getState().values['target-environment']?.['image-installer'];
|
||||
return isoTarget;
|
||||
};
|
||||
|
||||
return (
|
||||
<FormSpy>
|
||||
{() => (
|
||||
<>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Configure partitions</Text>
|
||||
</TextContent>
|
||||
{getState()?.values?.['file-system-configuration']?.find((mp) =>
|
||||
mp.mountpoint.includes('/usr')
|
||||
) && <UsrSubDirectoriesDisabled />}
|
||||
{rows.length > 1 &&
|
||||
getState()?.errors?.['file-system-configuration']?.duplicates
|
||||
?.length !== 0 &&
|
||||
showErrors() && (
|
||||
<Alert
|
||||
variant="danger"
|
||||
isInline
|
||||
title="Duplicate mount points: All mount points must be unique. Remove the duplicate or choose a new mount point."
|
||||
data-testid="fsc-warning"
|
||||
/>
|
||||
)}
|
||||
{rows.length >= 1 &&
|
||||
getState()?.errors?.['file-system-configuration']?.root === false &&
|
||||
showErrors() && (
|
||||
<Alert
|
||||
variant="danger"
|
||||
isInline
|
||||
title="No root partition configured."
|
||||
/>
|
||||
)}
|
||||
<TextContent>
|
||||
<Text>
|
||||
Create partitions for your image by defining mount points and
|
||||
minimum sizes. Image builder creates partitions with a logical
|
||||
volume (LVM) device type.
|
||||
</Text>
|
||||
<Text>
|
||||
The order of partitions may change when the image is installed in
|
||||
order to conform to best practices and ensure functionality.
|
||||
<br></br>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
href={FILE_SYSTEM_CUSTOMIZATION_URL}
|
||||
className="pf-u-pl-0"
|
||||
>
|
||||
Read more about manual configuration here
|
||||
</Button>
|
||||
</Text>
|
||||
</TextContent>
|
||||
{hasIsoTarget() && (
|
||||
<Alert
|
||||
variant="warning"
|
||||
isInline
|
||||
title="Filesystem customizations are not applied to 'Bare metal - Installer' images"
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
aria-label="File system table"
|
||||
className={isDragging && styles.modifiers.dragOver}
|
||||
variant="compact"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th aria-label="Drag mount point" />
|
||||
<Th>Mount point</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>
|
||||
Minimum size
|
||||
<Popover
|
||||
hasAutoWidth
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Image Builder may extend this size based on
|
||||
requirements, selected packages, and configurations.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="File system configuration info"
|
||||
aria-describedby="file-system-configuration-info"
|
||||
className="pf-c-form__group-label-help"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</Th>
|
||||
<Th aria-label="Remove mount point" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody
|
||||
ref={bodyref}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
data-testid="file-system-configuration-tbody"
|
||||
>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<Tr
|
||||
key={rowIndex}
|
||||
id={row.id}
|
||||
draggable
|
||||
onDrop={onDrop}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragStart={onDragStart}
|
||||
>
|
||||
<Td
|
||||
draggableRow={{
|
||||
id: `draggable-row-${row.id}`,
|
||||
}}
|
||||
/>
|
||||
<Td className="pf-m-width-30">
|
||||
<MountPoint
|
||||
key={row.id + '-mountpoint'}
|
||||
mountpoint={row.mountpoint}
|
||||
onChange={(mp) => setMountpoint(row.id, mp)}
|
||||
/>
|
||||
{getState().errors['file-system-configuration']?.duplicates
|
||||
.length !== 0 &&
|
||||
getState().errors[
|
||||
'file-system-configuration'
|
||||
]?.duplicates.indexOf(row.mountpoint) !== -1 &&
|
||||
showErrors() && (
|
||||
<Alert
|
||||
variant="danger"
|
||||
isInline
|
||||
isPlain
|
||||
title="Duplicate mount point."
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="pf-m-width-20">
|
||||
{/* always xfs */}
|
||||
{row.fstype}
|
||||
</Td>
|
||||
<Td className="pf-m-width-30">
|
||||
<SizeUnit
|
||||
key={row.id + '-sizeunit'}
|
||||
size={row.size}
|
||||
unit={row.unit}
|
||||
onChange={(s, u) => setSize(row.id, s, u)}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="pf-m-width-10">
|
||||
<Button
|
||||
variant="link"
|
||||
icon={<MinusCircleIcon />}
|
||||
onClick={() => removeRow(row.id)}
|
||||
data-testid="remove-mount-point"
|
||||
isDisabled={row.mountpoint === '/' ? true : false}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<TextContent>
|
||||
<Button
|
||||
ouiaId="add-partition"
|
||||
data-testid="file-system-add-partition"
|
||||
className="pf-u-text-align-left"
|
||||
variant="link"
|
||||
icon={<PlusCircleIcon />}
|
||||
onClick={addRow}
|
||||
>
|
||||
Add partition
|
||||
</Button>
|
||||
</TextContent>
|
||||
</>
|
||||
)}
|
||||
</FormSpy>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemConfiguration;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useFormApi } from '@data-driven-forms/react-form-renderer';
|
||||
import { Gallery, GalleryItem } from '@patternfly/react-core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const GalleryLayout = ({ fields, minWidths, maxWidths }) => {
|
||||
const { renderForm } = useFormApi();
|
||||
|
||||
return (
|
||||
<Gallery minWidths={minWidths} maxWidths={maxWidths} hasGutter>
|
||||
{fields.map((field) => (
|
||||
<GalleryItem key={field.name}>{renderForm([field])}</GalleryItem>
|
||||
))}
|
||||
</Gallery>
|
||||
);
|
||||
};
|
||||
|
||||
GalleryLayout.propTypes = {
|
||||
fields: PropTypes.array,
|
||||
maxWidths: PropTypes.object,
|
||||
minWidths: PropTypes.object,
|
||||
};
|
||||
|
||||
export default GalleryLayout;
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { FormSpy } from '@data-driven-forms/react-form-renderer';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
RELEASES,
|
||||
RHEL_8,
|
||||
RHEL_8_FULL_SUPPORT,
|
||||
RHEL_8_MAINTENANCE_SUPPORT,
|
||||
RHEL_9,
|
||||
RHEL_9_FULL_SUPPORT,
|
||||
RHEL_9_MAINTENANCE_SUPPORT,
|
||||
} from '../../../constants';
|
||||
import isRhel from '../../../Utilities/isRhel';
|
||||
import { toMonthAndYear } from '../../../Utilities/time';
|
||||
|
||||
const ImageOutputReleaseSelect = ({ label, isRequired, ...props }) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showDevelopmentOptions, setShowDevelopmentOptions] = useState(false);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Used to set release to RHEL 8 via search parameter, used by Insights assistant
|
||||
const preloadRelease = searchParams.get('release');
|
||||
useEffect(() => {
|
||||
preloadRelease === 'rhel8' && change(input.name, RHEL_8);
|
||||
}, [change, input.name, preloadRelease]);
|
||||
|
||||
const setRelease = (_, selection) => {
|
||||
change(input.name, selection);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleExpand = () => {
|
||||
setShowDevelopmentOptions(true);
|
||||
};
|
||||
|
||||
const setDescription = (key) => {
|
||||
let fullSupportEnd = '';
|
||||
let maintenanceSupportEnd = '';
|
||||
|
||||
if (key === RHEL_8) {
|
||||
fullSupportEnd = toMonthAndYear(RHEL_8_FULL_SUPPORT[1]);
|
||||
maintenanceSupportEnd = toMonthAndYear(RHEL_8_MAINTENANCE_SUPPORT[1]);
|
||||
}
|
||||
|
||||
if (key === RHEL_9) {
|
||||
fullSupportEnd = toMonthAndYear(RHEL_9_FULL_SUPPORT[1]);
|
||||
maintenanceSupportEnd = toMonthAndYear(RHEL_9_MAINTENANCE_SUPPORT[1]);
|
||||
}
|
||||
|
||||
if (isRhel(key)) {
|
||||
return `Full support ends: ${fullSupportEnd} | Maintenance support ends: ${maintenanceSupportEnd}`;
|
||||
}
|
||||
};
|
||||
|
||||
const setSelectOptions = () => {
|
||||
var options = [];
|
||||
const filteredRhel = new Map(
|
||||
[...RELEASES].filter(([key]) => {
|
||||
// Only show non-RHEL distros if expanded
|
||||
if (showDevelopmentOptions) {
|
||||
return true;
|
||||
}
|
||||
return isRhel(key);
|
||||
})
|
||||
);
|
||||
|
||||
filteredRhel.forEach((value, key) => {
|
||||
options.push(
|
||||
<SelectOption key={value} value={key} description={setDescription(key)}>
|
||||
{RELEASES.get(key)}
|
||||
</SelectOption>
|
||||
);
|
||||
});
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
return (
|
||||
<FormSpy>
|
||||
{() => (
|
||||
<FormGroup isRequired={isRequired} label={label}>
|
||||
<Select
|
||||
ouiaId="release_select"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setIsOpen(!isOpen)}
|
||||
onSelect={setRelease}
|
||||
selections={RELEASES.get(getState()?.values?.[input.name])}
|
||||
isOpen={isOpen}
|
||||
{...(!showDevelopmentOptions && {
|
||||
loadingVariant: {
|
||||
text: 'Show options for further development of RHEL',
|
||||
onClick: handleExpand,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{setSelectOptions()}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FormSpy>
|
||||
);
|
||||
};
|
||||
|
||||
ImageOutputReleaseSelect.propTypes = {
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ImageOutputReleaseSelect;
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import { Grid, GridItem, TextInput } from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const MountPointValidPrefixes = [
|
||||
'/app',
|
||||
'/boot',
|
||||
'/data',
|
||||
'/home',
|
||||
'/opt',
|
||||
'/srv',
|
||||
'/tmp',
|
||||
'/usr',
|
||||
'/var',
|
||||
];
|
||||
|
||||
const MountPoint = ({ ...props }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [prefix, setPrefix] = useState('/');
|
||||
const [suffix, setSuffix] = useState('');
|
||||
|
||||
// split
|
||||
useEffect(() => {
|
||||
for (const p of MountPointValidPrefixes) {
|
||||
if (props.mountpoint.startsWith(p)) {
|
||||
setPrefix(p);
|
||||
setSuffix(props.mountpoint.substring(p.length));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [props.mountpoint]);
|
||||
|
||||
useEffect(() => {
|
||||
let suf = suffix;
|
||||
let mp = prefix;
|
||||
if (suf) {
|
||||
if (mp !== '/' && suf[0] !== '/') {
|
||||
suf = '/' + suf;
|
||||
}
|
||||
|
||||
mp += suf;
|
||||
}
|
||||
|
||||
props.onChange(path.normalize(mp));
|
||||
}, [prefix, suffix]);
|
||||
|
||||
const onToggle = (isOpen) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
const onSelect = (event, selection) => {
|
||||
setPrefix(selection);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
// TODO make these stack vertically for xs viewport
|
||||
<Grid>
|
||||
<GridItem span={6}>
|
||||
<Select
|
||||
ouiaId="mount-point"
|
||||
isOpen={isOpen}
|
||||
onToggle={(_event, isOpen) => onToggle(isOpen)}
|
||||
onSelect={onSelect}
|
||||
selections={prefix}
|
||||
variant={SelectVariant.single}
|
||||
isDisabled={prefix === '/' ? true : false}
|
||||
>
|
||||
{MountPointValidPrefixes.map((pfx, index) => {
|
||||
return <SelectOption key={index} value={pfx} />;
|
||||
})}
|
||||
</Select>
|
||||
</GridItem>
|
||||
<GridItem span={6}>
|
||||
{prefix !== '/' &&
|
||||
!prefix.startsWith('/boot') &&
|
||||
!prefix.startsWith('/usr') && (
|
||||
<TextInput
|
||||
ouiaId="mount-suffix"
|
||||
type="text"
|
||||
value={suffix}
|
||||
aria-label="Mount point suffix text input"
|
||||
onChange={(_event, v) => setSuffix(v)}
|
||||
/>
|
||||
)}
|
||||
</GridItem>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
MountPoint.propTypes = {
|
||||
mountpoint: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default MountPoint;
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import useFieldApi, {
|
||||
UseFieldApiConfig,
|
||||
} from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import {
|
||||
Alert,
|
||||
FormGroup,
|
||||
Spinner,
|
||||
Popover,
|
||||
TextContent,
|
||||
Text,
|
||||
Button,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import { HelpIcon } from '@patternfly/react-icons';
|
||||
|
||||
import OscapProfileInformation from './OscapProfileInformation';
|
||||
|
||||
import {
|
||||
DistributionProfileItem,
|
||||
useGetOscapCustomizationsQuery,
|
||||
useGetOscapProfilesQuery,
|
||||
} from '../../../store/imageBuilderApi';
|
||||
import { reinitFileSystemConfiguratioStep } from '../steps/fileSystemConfiguration';
|
||||
import { reinitPackagesStep } from '../steps/packages';
|
||||
|
||||
type ChangeType = <F extends keyof Record<string, string | undefined>>(
|
||||
name: F,
|
||||
value?: Record<string, string | undefined>[F]
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Every time there is change on this form step's state, reinitialise the steps
|
||||
* that are depending on it. This will ensure that if the user goes back and
|
||||
* change their mind, going forward again leaves them in a coherent and workable
|
||||
* form state.
|
||||
*/
|
||||
const reinitDependingSteps = (change: ChangeType) => {
|
||||
reinitFileSystemConfiguratioStep(change);
|
||||
reinitPackagesStep(change);
|
||||
};
|
||||
type ProfileSelectorProps = {
|
||||
input: { name: string };
|
||||
};
|
||||
/**
|
||||
* Component for the user to select the profile to apply to their image.
|
||||
* The selected profile will be stored in the `oscap-profile` form state variable.
|
||||
* The Component is shown or not depending on the ShowSelector variable.
|
||||
*/
|
||||
|
||||
const ProfileSelector = ({ input }: ProfileSelectorProps) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const [profileName, setProfileName] = useState<string>('None');
|
||||
const [profile, setProfile] = useState(getState()?.values?.['oscap-profile']);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const {
|
||||
data: profiles,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
refetch,
|
||||
} = useGetOscapProfilesQuery({
|
||||
distribution: getState()?.values?.['release'],
|
||||
});
|
||||
|
||||
const { data } = useGetOscapCustomizationsQuery(
|
||||
{
|
||||
distribution: getState()?.values?.['release'],
|
||||
profile: profile,
|
||||
},
|
||||
{
|
||||
skip: !profile,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
data &&
|
||||
data.openscap &&
|
||||
typeof data.openscap.profile_name === 'string'
|
||||
) {
|
||||
setProfileName(data.openscap.profile_name);
|
||||
}
|
||||
if (data?.kernel) {
|
||||
change('kernel', data.kernel);
|
||||
}
|
||||
if (data?.services?.enabled) {
|
||||
change('enabledServices', data.services.enabled);
|
||||
}
|
||||
if (data?.services?.masked) {
|
||||
change('maskedServices', data.services.masked);
|
||||
}
|
||||
}, [data, change]);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!isOpen) {
|
||||
refetch();
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setProfile(undefined);
|
||||
change(input.name, undefined);
|
||||
change('kernel', undefined);
|
||||
change('enabledServices', undefined);
|
||||
change('maskedServices', undefined);
|
||||
setProfileName('');
|
||||
reinitDependingSteps(change);
|
||||
};
|
||||
|
||||
const handleSelect = (_: React.MouseEvent, selection: string) => {
|
||||
setProfile(selection);
|
||||
setIsOpen(false);
|
||||
change(input.name, selection);
|
||||
reinitDependingSteps(change);
|
||||
change('file-system-config-radio', 'manual');
|
||||
};
|
||||
|
||||
const options = [
|
||||
<OScapNoneOption setProfileName={setProfileName} key="oscap-none-option" />,
|
||||
];
|
||||
if (isSuccess) {
|
||||
options.concat(
|
||||
profiles.map((profile_id) => {
|
||||
return (
|
||||
<OScapSelectOption
|
||||
key={profile_id}
|
||||
profile_id={profile_id}
|
||||
setProfileName={setProfileName}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isFetching) {
|
||||
options.push(
|
||||
<SelectOption
|
||||
isNoResultsOption={true}
|
||||
data-testid="policies-loading"
|
||||
key={'None'}
|
||||
>
|
||||
<Spinner size="md" />
|
||||
</SelectOption>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
isRequired={true}
|
||||
data-testid="profiles-form-group"
|
||||
label={
|
||||
<>
|
||||
OpenSCAP profile
|
||||
<Popover
|
||||
maxWidth="30rem"
|
||||
position="left"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
To run a manual compliance scan in OpenSCAP, download this
|
||||
image.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button variant="plain" aria-label="About OpenSCAP" isInline>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
ouiaId="profileSelect"
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={handleToggle}
|
||||
onSelect={handleSelect}
|
||||
onClear={handleClear}
|
||||
selections={profileName}
|
||||
isOpen={isOpen}
|
||||
placeholderText="Select a profile"
|
||||
typeAheadAriaLabel="Select a profile"
|
||||
isDisabled={!isSuccess}
|
||||
onFilter={(_event, value) => {
|
||||
if (profiles) {
|
||||
return [
|
||||
<OScapNoneOption
|
||||
setProfileName={setProfileName}
|
||||
key="oscap-none-option"
|
||||
/>,
|
||||
].concat(
|
||||
profiles
|
||||
// stig and stig_gui don't boot at the moment,
|
||||
// so we should filter them out
|
||||
.filter((profile_id) => {
|
||||
const brokenProfiles = [
|
||||
'xccdf_org.ssgproject.content_profile_stig',
|
||||
'xccdf_org.ssgproject.content_profile_stig_gui',
|
||||
];
|
||||
|
||||
return !brokenProfiles.includes(profile_id);
|
||||
})
|
||||
.map((profile_id, index) => {
|
||||
return (
|
||||
<OScapSelectOption
|
||||
key={index}
|
||||
profile_id={profile_id}
|
||||
setProfileName={setProfileName}
|
||||
input={value}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{options}
|
||||
</Select>
|
||||
{isError && (
|
||||
<Alert
|
||||
title="Error fetching the profiles"
|
||||
variant="danger"
|
||||
isPlain
|
||||
isInline
|
||||
>
|
||||
Cannot get the list of profiles
|
||||
</Alert>
|
||||
)}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
type OScapNoneOptionPropType = {
|
||||
setProfileName: (name: string) => void;
|
||||
};
|
||||
|
||||
const OScapNoneOption = ({ setProfileName }: OScapNoneOptionPropType) => {
|
||||
return (
|
||||
<SelectOption
|
||||
value={undefined}
|
||||
onClick={() => {
|
||||
setProfileName('None');
|
||||
}}
|
||||
>
|
||||
<p>{'None'}</p>
|
||||
</SelectOption>
|
||||
);
|
||||
};
|
||||
|
||||
type OScapSelectOptionPropType = {
|
||||
profile_id: DistributionProfileItem;
|
||||
setProfileName: (name: string) => void;
|
||||
input?: string;
|
||||
};
|
||||
|
||||
const OScapSelectOption = ({
|
||||
profile_id,
|
||||
setProfileName,
|
||||
input,
|
||||
}: OScapSelectOptionPropType) => {
|
||||
const { getState } = useFormApi();
|
||||
const { data } = useGetOscapCustomizationsQuery({
|
||||
distribution: getState()?.values?.['release'],
|
||||
profile: profile_id,
|
||||
});
|
||||
|
||||
if (
|
||||
input &&
|
||||
!data?.openscap?.profile_name?.toLowerCase().includes(input.toLowerCase())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectOption
|
||||
key={profile_id}
|
||||
value={profile_id}
|
||||
onClick={() => {
|
||||
if (data?.openscap?.profile_name) {
|
||||
setProfileName(data.openscap.profile_name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p>{data?.openscap?.profile_name}</p>
|
||||
</SelectOption>
|
||||
);
|
||||
};
|
||||
|
||||
type ProfileTypeProp = {
|
||||
input: { name: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to prompt the use with two choices:
|
||||
* - to add a profile, in which case the ProfileSelector will allow the user to
|
||||
* pick a profile to be stored in the `oscap-profile` variable.
|
||||
* - to not add a profile, in which case the `oscap-profile` form state goes
|
||||
* undefined.
|
||||
*/
|
||||
const AddProfile = ({ input }: ProfileTypeProp) => {
|
||||
return (
|
||||
<>
|
||||
<ProfileSelector input={input} />
|
||||
<OscapProfileInformation />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface OscapProps extends UseFieldApiConfig {}
|
||||
|
||||
export const Oscap = (props: OscapProps) => {
|
||||
const { input } = useFieldApi(props);
|
||||
return <AddProfile input={input} />;
|
||||
};
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useFormApi } from '@data-driven-forms/react-form-renderer';
|
||||
import {
|
||||
Spinner,
|
||||
TextContent,
|
||||
TextList,
|
||||
TextListItem,
|
||||
TextListItemVariants,
|
||||
TextListVariants,
|
||||
CodeBlock,
|
||||
CodeBlockCode,
|
||||
Alert,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { RELEASES } from '../../../constants';
|
||||
import { useGetOscapCustomizationsQuery } from '../../../store/imageBuilderApi';
|
||||
|
||||
const OscapProfileInformation = (): JSX.Element => {
|
||||
const { getState } = useFormApi();
|
||||
|
||||
const oscapProfile = getState()?.values?.['oscap-profile'];
|
||||
|
||||
const {
|
||||
data: oscapProfileInfo,
|
||||
isFetching: isFetchingOscapProfileInfo,
|
||||
isSuccess: isSuccessOscapProfileInfo,
|
||||
} = useGetOscapCustomizationsQuery(
|
||||
{ distribution: getState()?.values?.['release'], profile: oscapProfile },
|
||||
{
|
||||
skip: !oscapProfile,
|
||||
}
|
||||
);
|
||||
|
||||
const enabledServicesDisplayString =
|
||||
oscapProfileInfo?.services?.enabled?.join(' ');
|
||||
const maskedServicesDisplayString =
|
||||
oscapProfileInfo?.services?.masked?.join(' ');
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFetchingOscapProfileInfo && <Spinner size="lg" />}
|
||||
{isSuccessOscapProfileInfo && (
|
||||
<>
|
||||
<TextContent>
|
||||
<br />
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Profile description:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{oscapProfileInfo.openscap?.profile_description}
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Operating system:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{RELEASES.get(getState()?.values?.['release'])}
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Reference ID:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{oscapProfileInfo.openscap?.profile_id}
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Kernel arguments:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
<CodeBlock>
|
||||
<CodeBlockCode>
|
||||
{oscapProfileInfo?.kernel?.append}
|
||||
</CodeBlockCode>
|
||||
</CodeBlock>
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Disabled services:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
<CodeBlock>
|
||||
<CodeBlockCode>{maskedServicesDisplayString}</CodeBlockCode>
|
||||
</CodeBlock>
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Enabled services:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
<CodeBlock>
|
||||
<CodeBlockCode>{enabledServicesDisplayString}</CodeBlockCode>
|
||||
</CodeBlock>
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
|
||||
<Alert
|
||||
variant="info"
|
||||
isInline
|
||||
isPlain
|
||||
title="Additional customizations"
|
||||
>
|
||||
Selecting an OpenSCAP profile will cause the appropriate packages,
|
||||
file system configuration, kernel arguments, and services to be
|
||||
added to your image.
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OscapProfileInformation;
|
||||
|
|
@ -1,533 +0,0 @@
|
|||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
|
||||
import {
|
||||
Alert,
|
||||
Divider,
|
||||
DualListSelector,
|
||||
DualListSelectorControl,
|
||||
DualListSelectorControlsWrapper,
|
||||
DualListSelectorList,
|
||||
DualListSelectorListItem,
|
||||
DualListSelectorPane,
|
||||
SearchInput,
|
||||
TextContent,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
AngleDoubleLeftIcon,
|
||||
AngleDoubleRightIcon,
|
||||
AngleLeftIcon,
|
||||
AngleRightIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import api from '../../../api';
|
||||
import {
|
||||
useGetArchitecturesQuery,
|
||||
useGetOscapCustomizationsQuery,
|
||||
} from '../../../store/imageBuilderApi';
|
||||
|
||||
const ExactMatch = ({
|
||||
pkgList,
|
||||
search,
|
||||
chosenPackages,
|
||||
selectedAvailablePackages,
|
||||
handleSelectAvailableFunc,
|
||||
}) => {
|
||||
const match = pkgList.find((pkg) => pkg.name === search);
|
||||
return (
|
||||
<DualListSelectorListItem
|
||||
data-testid={`exact-match-${match.name}`}
|
||||
isDisabled={chosenPackages[match.name] ? true : false}
|
||||
isSelected={selectedAvailablePackages.has(match.name)}
|
||||
onOptionSelect={(e) => handleSelectAvailableFunc(e, match.name)}
|
||||
>
|
||||
<TextContent key={`${match.name}`}>
|
||||
<small className="pf-u-mb-sm">Exact match</small>
|
||||
<span className="pf-c-dual-list-selector__item-text">{match.name}</span>
|
||||
<small>{match.summary}</small>
|
||||
<Divider />
|
||||
</TextContent>
|
||||
</DualListSelectorListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const RedHatPackages = ({ defaultArch }) => {
|
||||
const { getState } = useFormApi();
|
||||
const distribution = getState()?.values?.release;
|
||||
const arch = getState()?.values?.arch;
|
||||
const { data: distributionInformation, isSuccess: isSuccessDistroInfo } =
|
||||
useGetArchitecturesQuery({ distribution });
|
||||
|
||||
const getAllPackages = async (packagesSearchName) => {
|
||||
// if the env is stage beta then use content-sources api
|
||||
// else use image-builder api
|
||||
if (getState()?.values?.contentSourcesEnabled) {
|
||||
const filteredByArch = distributionInformation.find(
|
||||
(info) => info.arch === arch
|
||||
);
|
||||
const repoUrls = filteredByArch.repositories.map((repo) => repo.baseurl);
|
||||
return await api.getPackagesContentSources(repoUrls, packagesSearchName);
|
||||
} else {
|
||||
const args = [
|
||||
getState()?.values?.release,
|
||||
getState()?.values?.architecture || defaultArch,
|
||||
packagesSearchName,
|
||||
];
|
||||
const response = await api.getPackages(...args);
|
||||
let { data } = response;
|
||||
const { meta } = response;
|
||||
if (data?.length === meta.count) {
|
||||
return data;
|
||||
} else if (data) {
|
||||
({ data } = await api.getPackages(...args, meta.count));
|
||||
return data;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Packages getAllPackages={getAllPackages} isSuccess={isSuccessDistroInfo} />
|
||||
);
|
||||
};
|
||||
|
||||
export const ContentSourcesPackages = () => {
|
||||
const { getState } = useFormApi();
|
||||
|
||||
const getAllPackages = async (packagesSearchName) => {
|
||||
const repos = getState()?.values?.['payload-repositories'];
|
||||
const repoUrls = repos?.map((repo) => repo.baseurl);
|
||||
return await api.getPackagesContentSources(repoUrls, packagesSearchName);
|
||||
};
|
||||
|
||||
return <Packages getAllPackages={getAllPackages} />;
|
||||
};
|
||||
|
||||
const Packages = ({ getAllPackages, isSuccess }) => {
|
||||
const { currentStep } = useContext(WizardContext);
|
||||
const { change, getState } = useFormApi();
|
||||
const [packagesSearchName, setPackagesSearchName] = useState(undefined);
|
||||
const [filterChosen, setFilterChosen] = useState('');
|
||||
const [chosenPackages, setChosenPackages] = useState({});
|
||||
const [focus, setFocus] = useState('');
|
||||
const selectedPackages = getState()?.values?.['selected-packages'];
|
||||
const [availablePackages, setAvailablePackages] = useState(undefined);
|
||||
const [selectedAvailablePackages, setSelectedAvailablePackages] = useState(
|
||||
new Set()
|
||||
);
|
||||
const [selectedChosenPackages, setSelectedChosenPackages] = useState(
|
||||
new Set()
|
||||
);
|
||||
const firstInputElement = useRef(null);
|
||||
|
||||
const oscapProfile = getState()?.values?.['oscap-profile'];
|
||||
|
||||
const { data: customizations, isSuccess: isSuccessCustomizations } =
|
||||
useGetOscapCustomizationsQuery(
|
||||
{
|
||||
distribution: getState()?.values?.['release'],
|
||||
profile: oscapProfile,
|
||||
},
|
||||
{
|
||||
skip: !oscapProfile,
|
||||
}
|
||||
);
|
||||
useEffect(() => {
|
||||
if (customizations && customizations.packages && isSuccessCustomizations) {
|
||||
const oscapPackages = {};
|
||||
for (const pkg of customizations.packages) {
|
||||
oscapPackages[pkg] = { name: pkg };
|
||||
}
|
||||
updateState(oscapPackages);
|
||||
}
|
||||
}, [customizations, isSuccessCustomizations, updateState]);
|
||||
|
||||
// this effect only triggers on mount
|
||||
useEffect(() => {
|
||||
if (selectedPackages) {
|
||||
const newChosenPackages = {};
|
||||
for (const pkg of selectedPackages) {
|
||||
newChosenPackages[pkg.name] = pkg;
|
||||
}
|
||||
setChosenPackages(newChosenPackages);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
firstInputElement.current?.focus();
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
const searchResultsComparator = useCallback((searchTerm) => {
|
||||
return (a, b) => {
|
||||
a = a.name.toLowerCase();
|
||||
b = b.name.toLowerCase();
|
||||
|
||||
// check exact match first
|
||||
if (a === 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;
|
||||
}
|
||||
|
||||
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 (b < a) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const availablePackagesDisplayList = useMemo(() => {
|
||||
if (availablePackages === undefined) {
|
||||
return [];
|
||||
}
|
||||
const availablePackagesList = Object.values(availablePackages).sort(
|
||||
searchResultsComparator(packagesSearchName)
|
||||
);
|
||||
return availablePackagesList;
|
||||
}, [availablePackages, packagesSearchName, searchResultsComparator]);
|
||||
|
||||
const chosenPackagesDisplayList = useMemo(() => {
|
||||
const chosenPackagesList = Object.values(chosenPackages)
|
||||
.filter((pkg) => (pkg.name.includes(filterChosen) ? true : false))
|
||||
.sort(searchResultsComparator(filterChosen));
|
||||
return chosenPackagesList;
|
||||
}, [chosenPackages, filterChosen, searchResultsComparator]);
|
||||
|
||||
// call api to list available packages
|
||||
const handleAvailablePackagesSearch = async () => {
|
||||
const packageList = await getAllPackages(packagesSearchName);
|
||||
// If no packages are found, Image Builder returns null, while
|
||||
// Content Sources returns an empty array [].
|
||||
if (packageList) {
|
||||
const newAvailablePackages = {};
|
||||
for (const pkg of packageList) {
|
||||
newAvailablePackages[pkg.name] = pkg;
|
||||
}
|
||||
setAvailablePackages(newAvailablePackages);
|
||||
} else {
|
||||
setAvailablePackages([]);
|
||||
}
|
||||
};
|
||||
|
||||
const keydownHandler = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (focus === 'available') {
|
||||
event.stopPropagation();
|
||||
handleAvailablePackagesSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', keydownHandler, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keydownHandler, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateState = useCallback(
|
||||
(newChosenPackages) => {
|
||||
setSelectedAvailablePackages(new Set());
|
||||
setSelectedChosenPackages(new Set());
|
||||
setChosenPackages(newChosenPackages);
|
||||
change('selected-packages', Object.values(newChosenPackages));
|
||||
},
|
||||
[change]
|
||||
);
|
||||
|
||||
const moveSelectedToChosen = () => {
|
||||
const newChosenPackages = { ...chosenPackages };
|
||||
for (const pkgName of selectedAvailablePackages) {
|
||||
newChosenPackages[pkgName] = { ...availablePackages[pkgName] };
|
||||
}
|
||||
updateState(newChosenPackages);
|
||||
};
|
||||
|
||||
const moveAllToChosen = () => {
|
||||
const newChosenPackages = { ...chosenPackages, ...availablePackages };
|
||||
updateState(newChosenPackages);
|
||||
};
|
||||
|
||||
const removeSelectedFromChosen = () => {
|
||||
const newChosenPackages = {};
|
||||
for (const pkgName in chosenPackages) {
|
||||
if (!selectedChosenPackages.has(pkgName)) {
|
||||
newChosenPackages[pkgName] = { ...chosenPackages[pkgName] };
|
||||
}
|
||||
}
|
||||
updateState(newChosenPackages);
|
||||
};
|
||||
|
||||
const removeAllFromChosen = () => {
|
||||
const newChosenPackages = {};
|
||||
updateState(newChosenPackages);
|
||||
};
|
||||
|
||||
const handleSelectAvailable = (event, pkgName) => {
|
||||
const newSelected = new Set(selectedAvailablePackages);
|
||||
newSelected.has(pkgName)
|
||||
? newSelected.delete(pkgName)
|
||||
: newSelected.add(pkgName);
|
||||
setSelectedAvailablePackages(newSelected);
|
||||
};
|
||||
|
||||
const handleSelectChosen = (event, pkgName) => {
|
||||
const newSelected = new Set(selectedChosenPackages);
|
||||
newSelected.has(pkgName)
|
||||
? newSelected.delete(pkgName)
|
||||
: newSelected.add(pkgName);
|
||||
setSelectedChosenPackages(newSelected);
|
||||
};
|
||||
|
||||
const handleClearAvailableSearch = () => {
|
||||
setPackagesSearchName('');
|
||||
setAvailablePackages(undefined);
|
||||
};
|
||||
|
||||
const handleClearChosenSearch = () => {
|
||||
setFilterChosen('');
|
||||
};
|
||||
|
||||
return (
|
||||
<DualListSelector>
|
||||
<DualListSelectorPane
|
||||
title="Available packages"
|
||||
searchInput={
|
||||
<>
|
||||
<SearchInput
|
||||
placeholder="Search for a package"
|
||||
data-testid="search-available-pkgs-input"
|
||||
value={packagesSearchName}
|
||||
ref={firstInputElement}
|
||||
onFocus={() => setFocus('available')}
|
||||
onBlur={() => setFocus('')}
|
||||
onChange={(_, val) => setPackagesSearchName(val)}
|
||||
submitSearchButtonLabel="Search button for available packages"
|
||||
onSearch={handleAvailablePackagesSearch}
|
||||
resetButtonLabel="Clear available packages search"
|
||||
onClear={handleClearAvailableSearch}
|
||||
isDisabled={currentStep.name === 'packages' ? !isSuccess : false}
|
||||
/>
|
||||
{availablePackagesDisplayList.length >= 100 && (
|
||||
<Alert
|
||||
title="Over 100 results found. Refine your search."
|
||||
variant="warning"
|
||||
isPlain
|
||||
isInline
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
status={
|
||||
selectedAvailablePackages.size > 0
|
||||
? `${selectedAvailablePackages.size}
|
||||
of ${availablePackagesDisplayList.length} items`
|
||||
: `${availablePackagesDisplayList.length} items`
|
||||
}
|
||||
>
|
||||
<DualListSelectorList data-testid="available-pkgs-list">
|
||||
{availablePackages === undefined ? (
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
Search above to add additional
|
||||
<br />
|
||||
packages to your image
|
||||
</p>
|
||||
) : availablePackagesDisplayList.length === 0 ? (
|
||||
<>
|
||||
<p className="pf-u-text-align-center pf-u-mt-md pf-u-font-size-lg pf-u-font-weight-bold">
|
||||
No results found
|
||||
</p>
|
||||
<br />
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
Adjust your search and try again
|
||||
</p>
|
||||
</>
|
||||
) : availablePackagesDisplayList.length >= 100 ? (
|
||||
<>
|
||||
{availablePackagesDisplayList.some(
|
||||
(pkg) => pkg.name === packagesSearchName
|
||||
) && (
|
||||
<ExactMatch
|
||||
pkgList={availablePackagesDisplayList}
|
||||
search={packagesSearchName}
|
||||
chosenPackages={chosenPackages}
|
||||
selectedAvailablePackages={selectedAvailablePackages}
|
||||
handleSelectAvailableFunc={handleSelectAvailable}
|
||||
/>
|
||||
)}
|
||||
<p className="pf-u-text-align-center pf-u-mt-md pf-u-font-size-lg pf-u-font-weight-bold">
|
||||
Too many results to display
|
||||
</p>
|
||||
<br />
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
Please make the search more specific
|
||||
<br />
|
||||
and try again
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
availablePackagesDisplayList.map((pkg) => {
|
||||
return (
|
||||
<DualListSelectorListItem
|
||||
data-testid={`available-pkgs-${pkg.name}`}
|
||||
key={pkg.name}
|
||||
isDisabled={chosenPackages[pkg.name] ? true : false}
|
||||
isSelected={selectedAvailablePackages.has(pkg.name)}
|
||||
onOptionSelect={(e) => handleSelectAvailable(e, pkg.name)}
|
||||
>
|
||||
<TextContent key={`${pkg.name}`}>
|
||||
<span
|
||||
className={
|
||||
chosenPackages[pkg.name] && 'pf-v5-u-color-400'
|
||||
}
|
||||
>
|
||||
{pkg.name}
|
||||
</span>
|
||||
<small>{pkg.summary}</small>
|
||||
</TextContent>
|
||||
</DualListSelectorListItem>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</DualListSelectorList>
|
||||
</DualListSelectorPane>
|
||||
<DualListSelectorControlsWrapper aria-label="Selector controls">
|
||||
<DualListSelectorControl
|
||||
isDisabled={selectedAvailablePackages.size === 0}
|
||||
onClick={() => moveSelectedToChosen()}
|
||||
aria-label="Add selected"
|
||||
tooltipContent="Add selected"
|
||||
>
|
||||
<AngleRightIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
isDisabled={
|
||||
availablePackagesDisplayList.length === 0 ||
|
||||
// also disable the "Add all" button if there are too many matches
|
||||
// (even if there's an exact match)
|
||||
availablePackagesDisplayList.length >= 100
|
||||
}
|
||||
onClick={() => moveAllToChosen()}
|
||||
aria-label="Add all"
|
||||
tooltipContent="Add all"
|
||||
>
|
||||
<AngleDoubleRightIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
isDisabled={Object.values(chosenPackages).length === 0}
|
||||
onClick={() => removeAllFromChosen()}
|
||||
aria-label="Remove all"
|
||||
tooltipContent="Remove all"
|
||||
>
|
||||
<AngleDoubleLeftIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
onClick={() => removeSelectedFromChosen()}
|
||||
isDisabled={selectedChosenPackages.size === 0}
|
||||
aria-label="Remove selected"
|
||||
tooltipContent="Remove selected"
|
||||
>
|
||||
<AngleLeftIcon />
|
||||
</DualListSelectorControl>
|
||||
</DualListSelectorControlsWrapper>
|
||||
<DualListSelectorPane
|
||||
title="Chosen packages"
|
||||
searchInput={
|
||||
<SearchInput
|
||||
placeholder="Search for a package"
|
||||
data-testid="search-chosen-pkgs-input"
|
||||
value={filterChosen}
|
||||
onFocus={() => setFocus('chosen')}
|
||||
onBlur={() => setFocus('')}
|
||||
onChange={(_, val) => setFilterChosen(val)}
|
||||
resetButtonLabel="Clear chosen packages search"
|
||||
onClear={handleClearChosenSearch}
|
||||
/>
|
||||
}
|
||||
status={
|
||||
selectedChosenPackages.size > 0
|
||||
? `${selectedChosenPackages.size}
|
||||
of ${chosenPackagesDisplayList.length} items`
|
||||
: `${chosenPackagesDisplayList.length} items`
|
||||
}
|
||||
isChosen
|
||||
>
|
||||
<DualListSelectorList data-testid="chosen-pkgs-list">
|
||||
{Object.values(chosenPackages).length === 0 ? (
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
No packages added
|
||||
</p>
|
||||
) : chosenPackagesDisplayList.length === 0 ? (
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
No packages found
|
||||
</p>
|
||||
) : (
|
||||
chosenPackagesDisplayList.map((pkg) => {
|
||||
return (
|
||||
<DualListSelectorListItem
|
||||
data-testid={`selected-pkgs-${pkg.name}`}
|
||||
key={pkg.name}
|
||||
isSelected={selectedChosenPackages.has(pkg.name)}
|
||||
onOptionSelect={(e) => handleSelectChosen(e, pkg.name)}
|
||||
>
|
||||
<TextContent key={`${pkg.name}`}>
|
||||
<span className="pf-c-dual-list-selector__item-text">
|
||||
{pkg.name}
|
||||
</span>
|
||||
<small>{pkg.summary}</small>
|
||||
</TextContent>
|
||||
</DualListSelectorListItem>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</DualListSelectorList>
|
||||
</DualListSelectorPane>
|
||||
</DualListSelector>
|
||||
);
|
||||
};
|
||||
|
||||
ExactMatch.propTypes = {
|
||||
pkgList: PropTypes.arrayOf(PropTypes.object),
|
||||
search: PropTypes.string,
|
||||
chosenPackages: PropTypes.object,
|
||||
selectedAvailablePackages: PropTypes.object,
|
||||
handleSelectAvailableFunc: PropTypes.func,
|
||||
};
|
||||
|
||||
RedHatPackages.propTypes = {
|
||||
defaultArch: PropTypes.string,
|
||||
};
|
||||
|
||||
Packages.propTypes = {
|
||||
getAllPackages: PropTypes.func,
|
||||
isSuccess: PropTypes.bool,
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import React, { useRef } from 'react';
|
||||
|
||||
import Radio from '@data-driven-forms/pf4-component-mapper/radio';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const RadioWithPopover = ({ Popover, ...props }) => {
|
||||
const ref = useRef();
|
||||
return (
|
||||
<Radio
|
||||
{...props}
|
||||
label={
|
||||
<span ref={ref} className="ins-c-image--builder__popover">
|
||||
{props.label}
|
||||
<Popover />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
RadioWithPopover.propTypes = {
|
||||
Popover: PropTypes.elementType.isRequired,
|
||||
label: PropTypes.node,
|
||||
};
|
||||
|
||||
export default RadioWithPopover;
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { FormSpy } from '@data-driven-forms/react-form-renderer';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
Popover,
|
||||
Radio,
|
||||
Text,
|
||||
TextContent,
|
||||
} from '@patternfly/react-core';
|
||||
import { HelpIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
INSIGHTS_URL,
|
||||
RHC_URL,
|
||||
SUBSCRIPTION_MANAGEMENT_URL,
|
||||
} from '../../../constants';
|
||||
|
||||
const RHSMPopover = () => {
|
||||
return (
|
||||
<Popover
|
||||
headerContent="About Red Hat Subscription Management"
|
||||
position="right"
|
||||
minWidth="30rem"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Registered systems are entitled to support services, errata,
|
||||
patches, and upgrades.
|
||||
</Text>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={SUBSCRIPTION_MANAGEMENT_URL}
|
||||
>
|
||||
Learn more about Red Hat Subscription Management
|
||||
</Button>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
className="pf-c-form__group-label-help"
|
||||
aria-label="About remote host configuration (rhc)"
|
||||
isInline
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const InsightsPopover = () => {
|
||||
return (
|
||||
<Popover
|
||||
headerContent="About Red Hat Insights"
|
||||
position="right"
|
||||
minWidth="30rem"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Red Hat Insights client provides actionable intelligence about your
|
||||
Red Hat Enterprise Linux environments, helping to identify and
|
||||
address operational and vulnerability risks before an issue results
|
||||
in downtime.
|
||||
</Text>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={INSIGHTS_URL}
|
||||
>
|
||||
Learn more about Red Hat Insights
|
||||
</Button>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
className="pf-c-form__group-label-help"
|
||||
aria-label="About remote host configuration (rhc)"
|
||||
isInline
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const RhcPopover = () => {
|
||||
return (
|
||||
<Popover
|
||||
headerContent="About remote host configuration (rhc)"
|
||||
position="right"
|
||||
minWidth="30rem"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Remote host configuration allows Red Hat Enterprise Linux hosts to
|
||||
connect to Red Hat Insights. Remote host configuration is required
|
||||
to use the Red Hat Insights Remediations service.
|
||||
</Text>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={RHC_URL}
|
||||
>
|
||||
Learn more about remote host configuration
|
||||
</Button>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
className="pf-c-form__group-label-help"
|
||||
aria-label="About remote host configuration (rhc)"
|
||||
isInline
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const Registration = ({ label, ...props }) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const registerSystem = getState()?.values?.['register-system'];
|
||||
const [showOptions, setShowOptions] = useState(
|
||||
registerSystem === 'register-now-insights' ||
|
||||
registerSystem === 'register-now'
|
||||
);
|
||||
|
||||
return (
|
||||
<FormSpy>
|
||||
{() => (
|
||||
<FormGroup label={label}>
|
||||
<Radio
|
||||
autoFocus
|
||||
label={
|
||||
(!showOptions &&
|
||||
'Automatically register and enable advanced capabilities') || (
|
||||
<>
|
||||
Monitor & manage subscriptions and access to Red Hat content
|
||||
<RHSMPopover />
|
||||
</>
|
||||
)
|
||||
}
|
||||
data-testid="registration-radio-now"
|
||||
name="register-system"
|
||||
id="register-system-now"
|
||||
isChecked={registerSystem.startsWith('register-now')}
|
||||
onChange={() => {
|
||||
change(input.name, 'register-now-rhc');
|
||||
}}
|
||||
description={
|
||||
!showOptions && (
|
||||
<Button
|
||||
component="a"
|
||||
data-testid="registration-additional-options"
|
||||
variant="link"
|
||||
isDisabled={!registerSystem.startsWith('register-now')}
|
||||
isInline
|
||||
onClick={() => setShowOptions(!showOptions)}
|
||||
>
|
||||
Show additional connection options
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
body={
|
||||
showOptions && (
|
||||
<Checkbox
|
||||
className="pf-u-ml-lg"
|
||||
label={
|
||||
<>
|
||||
Enable predictive analytics and management capabilities
|
||||
<InsightsPopover />
|
||||
</>
|
||||
}
|
||||
data-testid="registration-checkbox-insights"
|
||||
isChecked={
|
||||
registerSystem === 'register-now-insights' ||
|
||||
registerSystem === 'register-now-rhc'
|
||||
}
|
||||
onChange={(_event, checked) => {
|
||||
if (checked) {
|
||||
change(input.name, 'register-now-insights');
|
||||
} else {
|
||||
change(input.name, 'register-now');
|
||||
}
|
||||
}}
|
||||
id="register-system-now-insights"
|
||||
name="register-system-insights"
|
||||
body={
|
||||
<Checkbox
|
||||
label={
|
||||
<>
|
||||
Enable remote remediations and system management with
|
||||
automation
|
||||
<RhcPopover />
|
||||
</>
|
||||
}
|
||||
data-testid="registration-checkbox-rhc"
|
||||
isChecked={registerSystem === 'register-now-rhc'}
|
||||
onChange={(_event, checked) => {
|
||||
if (checked) {
|
||||
change(input.name, 'register-now-rhc');
|
||||
} else {
|
||||
change(input.name, 'register-now-insights');
|
||||
}
|
||||
}}
|
||||
id="register-system-now-rhc"
|
||||
name="register-system-rhc"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Radio
|
||||
name="register-system"
|
||||
className="pf-u-mt-md"
|
||||
data-testid="registration-radio-later"
|
||||
id="register-system-later"
|
||||
label="Register later"
|
||||
isChecked={registerSystem === 'register-later'}
|
||||
onChange={() => {
|
||||
setShowOptions(false);
|
||||
change(input.name, 'register-later');
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FormSpy>
|
||||
);
|
||||
};
|
||||
|
||||
Registration.propTypes = {
|
||||
label: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Registration;
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { FormSpy } from '@data-driven-forms/react-form-renderer';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import { isEmpty } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ActivationKeyInformation from './ActivationKeyInformation';
|
||||
|
||||
const RegistrationKeyInformation = ({ label, valueReference }) => {
|
||||
return (
|
||||
<FormSpy>
|
||||
{({ values }) =>
|
||||
isEmpty(values[valueReference]) ? null : (
|
||||
<FormGroup label={label}>
|
||||
<ActivationKeyInformation />
|
||||
</FormGroup>
|
||||
)
|
||||
}
|
||||
</FormSpy>
|
||||
);
|
||||
};
|
||||
|
||||
RegistrationKeyInformation.propTypes = {
|
||||
label: PropTypes.node,
|
||||
valueReference: PropTypes.node,
|
||||
};
|
||||
|
||||
export default RegistrationKeyInformation;
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
|
||||
import { useFormApi } from '@data-driven-forms/react-form-renderer';
|
||||
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
|
||||
import {
|
||||
Button,
|
||||
ExpandableSection,
|
||||
FormGroup,
|
||||
Panel,
|
||||
PanelMain,
|
||||
Text,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
|
||||
import {
|
||||
RELEASES,
|
||||
RELEASE_LIFECYCLE_URL,
|
||||
RHEL_8,
|
||||
RHEL_8_FULL_SUPPORT,
|
||||
RHEL_8_MAINTENANCE_SUPPORT,
|
||||
RHEL_9,
|
||||
RHEL_9_FULL_SUPPORT,
|
||||
RHEL_9_MAINTENANCE_SUPPORT,
|
||||
} from '../../../constants';
|
||||
import 'chartjs-adapter-moment';
|
||||
import { toMonthAndYear } from '../../../Utilities/time';
|
||||
|
||||
Chart.register(annotationPlugin);
|
||||
Chart.register(...registerables);
|
||||
|
||||
const currentDate = new Date().toISOString();
|
||||
|
||||
export const chartMajorVersionCfg = {
|
||||
data: {
|
||||
labels: ['RHEL 9', 'RHEL 8'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Full support',
|
||||
backgroundColor: '#0066CC',
|
||||
data: [
|
||||
{
|
||||
x: RHEL_9_FULL_SUPPORT,
|
||||
y: 'RHEL 9',
|
||||
},
|
||||
{
|
||||
x: RHEL_8_FULL_SUPPORT,
|
||||
y: 'RHEL 8',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Maintenance support',
|
||||
backgroundColor: '#8BC1F7',
|
||||
data: [
|
||||
{
|
||||
x: RHEL_9_MAINTENANCE_SUPPORT,
|
||||
y: 'RHEL 9',
|
||||
},
|
||||
{
|
||||
x: RHEL_8_MAINTENANCE_SUPPORT,
|
||||
y: 'RHEL 8',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y' as const,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time' as const,
|
||||
time: {
|
||||
unit: 'year' as const,
|
||||
},
|
||||
min: '2019-01-01' as const,
|
||||
max: '2033-01-01' as const,
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom' as const,
|
||||
},
|
||||
annotation: {
|
||||
annotations: {
|
||||
today: {
|
||||
type: 'line' as const,
|
||||
xMin: currentDate,
|
||||
xMax: currentDate,
|
||||
borderColor: 'black',
|
||||
borderWidth: 2,
|
||||
borderDash: [8, 2],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MajorReleasesLifecyclesChart = () => {
|
||||
return (
|
||||
<Panel>
|
||||
<PanelMain maxHeight="10rem">
|
||||
<Bar
|
||||
data-testid="release-lifecycle-chart"
|
||||
options={chartMajorVersionCfg.options}
|
||||
data={chartMajorVersionCfg.data}
|
||||
/>
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const ReleaseLifecycle = () => {
|
||||
const { getState } = useFormApi();
|
||||
const { currentStep } = useContext(WizardContext);
|
||||
const release = getState().values.release;
|
||||
const [isExpanded, setIsExpanded] = React.useState(true);
|
||||
|
||||
const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
|
||||
setIsExpanded(isExpanded);
|
||||
};
|
||||
|
||||
if (release === RHEL_8) {
|
||||
if (currentStep.name === 'image-output') {
|
||||
return (
|
||||
<ExpandableSection
|
||||
toggleText={
|
||||
isExpanded
|
||||
? 'Hide information about release lifecycle'
|
||||
: 'Show information about release lifecycle'
|
||||
}
|
||||
onToggle={onToggle}
|
||||
isExpanded={isExpanded}
|
||||
isIndented
|
||||
>
|
||||
<FormGroup label="Release lifecycle">
|
||||
<MajorReleasesLifecyclesChart />
|
||||
</FormGroup>
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={RELEASE_LIFECYCLE_URL}
|
||||
>
|
||||
View Red Hat Enterprise Linux Life Cycle dates
|
||||
</Button>
|
||||
</ExpandableSection>
|
||||
);
|
||||
} else if (currentStep.name === 'review') {
|
||||
return (
|
||||
<>
|
||||
<Text className="pf-v5-u-font-size-sm">
|
||||
{RELEASES.get(release)} will be supported through{' '}
|
||||
{toMonthAndYear(RHEL_8_FULL_SUPPORT[1])}, with optional ELS support
|
||||
through {toMonthAndYear(RHEL_8_MAINTENANCE_SUPPORT[1])}. Consider
|
||||
building an image with {RELEASES.get(RHEL_9)} to extend the support
|
||||
period.
|
||||
</Text>
|
||||
<FormGroup label="Release lifecycle">
|
||||
<MajorReleasesLifecyclesChart />
|
||||
</FormGroup>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default ReleaseLifecycle;
|
||||
|
|
@ -1,612 +0,0 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
useFieldApi,
|
||||
useFormApi,
|
||||
} from '@data-driven-forms/react-form-renderer';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
EmptyStateVariant,
|
||||
Pagination,
|
||||
Panel,
|
||||
PanelMain,
|
||||
SearchInput,
|
||||
Spinner,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarItem,
|
||||
EmptyStateHeader,
|
||||
EmptyStateFooter,
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
PaginationVariant,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownToggle,
|
||||
DropdownToggleCheckbox,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
import { RepositoryIcon } from '@patternfly/react-icons';
|
||||
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import RepositoriesStatus from './RepositoriesStatus';
|
||||
import RepositoryUnavailable from './RepositoryUnavailable';
|
||||
|
||||
import { useListRepositoriesQuery } from '../../../store/contentSourcesApi';
|
||||
import { releaseToVersion } from '../../../Utilities/releaseToVersion';
|
||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
||||
|
||||
const BulkSelect = ({
|
||||
selected,
|
||||
count,
|
||||
filteredCount,
|
||||
perPage,
|
||||
handleSelectAll,
|
||||
handleSelectPage,
|
||||
handleDeselectAll,
|
||||
isDisabled,
|
||||
}) => {
|
||||
const [dropdownIsOpen, setDropdownIsOpen] = useState(false);
|
||||
|
||||
const numSelected = selected.length;
|
||||
const allSelected = count !== 0 ? numSelected === count : undefined;
|
||||
const anySelected = numSelected > 0;
|
||||
const someChecked = anySelected ? null : false;
|
||||
const isChecked = allSelected ? true : someChecked;
|
||||
|
||||
const items = [
|
||||
<DropdownItem
|
||||
key="none"
|
||||
onClick={handleDeselectAll}
|
||||
>{`Select none (0 items)`}</DropdownItem>,
|
||||
<DropdownItem key="page" onClick={handleSelectPage}>{`Select page (${
|
||||
perPage > filteredCount ? filteredCount : perPage
|
||||
} items)`}</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="all"
|
||||
onClick={handleSelectAll}
|
||||
>{`Select all (${count} items)`}</DropdownItem>,
|
||||
];
|
||||
|
||||
const handleDropdownSelect = () => {};
|
||||
|
||||
const toggleDropdown = () => setDropdownIsOpen(!dropdownIsOpen);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
onSelect={handleDropdownSelect}
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
id="stacked-example-toggle"
|
||||
isDisabled={isDisabled}
|
||||
splitButtonItems={[
|
||||
<DropdownToggleCheckbox
|
||||
id="example-checkbox-1"
|
||||
key="split-checkbox"
|
||||
aria-label="Select all"
|
||||
isChecked={isChecked}
|
||||
onClick={() => {
|
||||
anySelected ? handleDeselectAll() : handleSelectAll();
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
onToggle={toggleDropdown}
|
||||
>
|
||||
{numSelected !== 0 ? `${numSelected} selected` : null}
|
||||
</DropdownToggle>
|
||||
}
|
||||
isOpen={dropdownIsOpen}
|
||||
dropdownItems={items}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Utility function to convert from Content Sources to Image Builder payload repo API schema
|
||||
const convertSchemaToIBPayloadRepo = (repo) => {
|
||||
const imageBuilderRepo = {
|
||||
baseurl: repo.url,
|
||||
rhsm: false,
|
||||
check_gpg: false,
|
||||
};
|
||||
// only include the flag if enabled
|
||||
if (repo.module_hotfixes) {
|
||||
imageBuilderRepo.module_hotfixes = repo.module_hotfixes;
|
||||
}
|
||||
if (repo.gpg_key) {
|
||||
imageBuilderRepo.gpgkey = repo.gpg_key;
|
||||
imageBuilderRepo.check_gpg = true;
|
||||
imageBuilderRepo.check_repo_gpg = repo.metadata_verification;
|
||||
}
|
||||
|
||||
return imageBuilderRepo;
|
||||
};
|
||||
|
||||
// Utility function to convert from Content Sources to Image Builder custom repo API schema
|
||||
const convertSchemaToIBCustomRepo = (repo) => {
|
||||
const imageBuilderRepo = {
|
||||
id: repo.uuid,
|
||||
name: repo.name,
|
||||
baseurl: [repo.url],
|
||||
check_gpg: false,
|
||||
};
|
||||
// only include the flag if enabled
|
||||
if (repo.module_hotfixes) {
|
||||
imageBuilderRepo.module_hotfixes = repo.module_hotfixes;
|
||||
}
|
||||
if (repo.gpg_key) {
|
||||
imageBuilderRepo.gpgkey = [repo.gpg_key];
|
||||
imageBuilderRepo.check_gpg = true;
|
||||
imageBuilderRepo.check_repo_gpg = repo.metadata_verification;
|
||||
}
|
||||
|
||||
return imageBuilderRepo;
|
||||
};
|
||||
|
||||
// Utility function to convert from Image Builder to Content Sources API schema
|
||||
const convertSchemaToContentSources = (repo) => {
|
||||
const contentSourcesRepo = {
|
||||
url: repo.baseurl,
|
||||
rhsm: false,
|
||||
};
|
||||
if (repo.gpgkey) {
|
||||
contentSourcesRepo.gpg_key = repo.gpgkey;
|
||||
contentSourcesRepo.metadata_verification = repo.check_repo_gpg;
|
||||
}
|
||||
|
||||
return contentSourcesRepo;
|
||||
};
|
||||
|
||||
const Repositories = (props) => {
|
||||
const initializeRepositories = (contentSourcesReposList) => {
|
||||
// Convert list of repositories into an object where key is repo URL
|
||||
const contentSourcesRepos = contentSourcesReposList.reduce(
|
||||
(accumulator, currentValue) => {
|
||||
accumulator[currentValue.url] = currentValue;
|
||||
return accumulator;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
// Repositories in the form state can be present when 'Recreate image' is used
|
||||
// to open the wizard that are not necessarily in content sources.
|
||||
const formStateReposList =
|
||||
getState()?.values?.['original-payload-repositories'];
|
||||
|
||||
const mergeRepositories = (contentSourcesRepos, formStateReposList) => {
|
||||
const formStateRepos = {};
|
||||
|
||||
for (const repo of formStateReposList) {
|
||||
formStateRepos[repo.baseurl] = convertSchemaToContentSources(repo);
|
||||
formStateRepos[repo.baseurl].name = '';
|
||||
}
|
||||
|
||||
// In case of duplicate repo urls, the repo from Content Sources overwrites the
|
||||
// repo from the form state.
|
||||
const mergedRepos = { ...formStateRepos, ...contentSourcesRepos };
|
||||
|
||||
return mergedRepos;
|
||||
};
|
||||
|
||||
const repositories = formStateReposList
|
||||
? mergeRepositories(contentSourcesRepos, formStateReposList)
|
||||
: contentSourcesRepos;
|
||||
|
||||
return repositories;
|
||||
};
|
||||
|
||||
const { getState, change } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
const [page, setPage] = useState(1);
|
||||
const [toggleSelected, setToggleSelected] = useState('toggle-group-all');
|
||||
const [selected, setSelected] = useState(
|
||||
getState()?.values?.['payload-repositories']
|
||||
? getState().values['payload-repositories'].map((repo) => repo.baseurl)
|
||||
: []
|
||||
);
|
||||
|
||||
const arch = getState().values?.arch;
|
||||
const release = getState().values?.release;
|
||||
const version = releaseToVersion(release);
|
||||
|
||||
const firstRequest = useListRepositoriesQuery(
|
||||
{
|
||||
availableForArch: arch,
|
||||
availableForVersion: version,
|
||||
contentType: 'rpm',
|
||||
origin: 'external',
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
},
|
||||
// The cached repos may be incorrect, for now refetch on mount to ensure that
|
||||
// they are accurate when this step loads. Future PR will implement prefetching
|
||||
// and this can be removed.
|
||||
{ refetchOnMountOrArgChange: true }
|
||||
);
|
||||
|
||||
const skip =
|
||||
firstRequest?.data?.meta?.count === undefined ||
|
||||
firstRequest?.data?.meta?.count <= 100;
|
||||
|
||||
// Fetch *all* repositories if there are more than 100 so that typeahead filter works
|
||||
const followupRequest = useListRepositoriesQuery(
|
||||
{
|
||||
availableForArch: arch,
|
||||
availableForVersion: version,
|
||||
contentType: 'rpm',
|
||||
origin: 'external',
|
||||
limit: firstRequest?.data?.meta?.count,
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
skip: skip,
|
||||
}
|
||||
);
|
||||
|
||||
const { data, isError, isFetching, isLoading, isSuccess, refetch } =
|
||||
useMemo(() => {
|
||||
if (firstRequest?.data?.meta?.count > 100) {
|
||||
return { ...followupRequest };
|
||||
}
|
||||
return { ...firstRequest };
|
||||
}, [firstRequest, followupRequest]);
|
||||
|
||||
const repositories = useMemo(() => {
|
||||
return data ? initializeRepositories(data.data) : {};
|
||||
}, [firstRequest.data, followupRequest.data]);
|
||||
|
||||
const handleToggleClick = (event) => {
|
||||
const id = event.currentTarget.id;
|
||||
setPage(1);
|
||||
setToggleSelected(id);
|
||||
};
|
||||
|
||||
const isRepoSelected = (repoURL) => selected.includes(repoURL);
|
||||
|
||||
const handlePerPageSelect = (event, newPerPage, newPage) => {
|
||||
setPerPage(newPerPage);
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleSetPage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
// filter displayed selected packages
|
||||
const handleFilterRepositories = (_, value) => {
|
||||
setPage(1);
|
||||
setFilterValue(value);
|
||||
};
|
||||
|
||||
const filteredRepositoryURLs = useMemo(() => {
|
||||
const repoUrls = Object.values(repositories).filter((repo) =>
|
||||
repo.name.toLowerCase().includes(filterValue.toLowerCase())
|
||||
);
|
||||
if (toggleSelected === 'toggle-group-all') {
|
||||
return repoUrls.map((repo) => repo.url);
|
||||
} else if (toggleSelected === 'toggle-group-selected') {
|
||||
return repoUrls
|
||||
.filter((repo) => isRepoSelected(repo.url))
|
||||
.map((repo) => repo.url);
|
||||
}
|
||||
}, [filterValue, repositories, toggleSelected]);
|
||||
|
||||
const handleClearFilter = () => {
|
||||
setFilterValue('');
|
||||
};
|
||||
|
||||
const updateFormState = (selectedRepoURLs) => {
|
||||
// repositories is stored as an object with repoURLs as keys
|
||||
const selectedRepos = [];
|
||||
for (const repoURL of selectedRepoURLs) {
|
||||
selectedRepos.push(repositories[repoURL]);
|
||||
}
|
||||
|
||||
const payloadRepositories = selectedRepos.map((repo) =>
|
||||
convertSchemaToIBPayloadRepo(repo)
|
||||
);
|
||||
|
||||
const customRepositories = selectedRepos.map((repo) =>
|
||||
convertSchemaToIBCustomRepo(repo)
|
||||
);
|
||||
|
||||
input.onChange(payloadRepositories);
|
||||
change('custom-repositories', customRepositories);
|
||||
};
|
||||
|
||||
const updateSelected = (selectedRepos) => {
|
||||
setSelected(selectedRepos);
|
||||
updateFormState(selectedRepos);
|
||||
};
|
||||
|
||||
const handleSelect = (repoURL, rowIndex, isSelecting) => {
|
||||
if (isSelecting === true) {
|
||||
updateSelected([...selected, repoURL]);
|
||||
} else if (isSelecting === false) {
|
||||
updateSelected(
|
||||
selected.filter((selectedRepoId) => selectedRepoId !== repoURL)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
updateSelected(Object.keys(repositories));
|
||||
};
|
||||
|
||||
const computeStart = () => perPage * (page - 1);
|
||||
const computeEnd = () => perPage * page;
|
||||
|
||||
const handleSelectPage = () => {
|
||||
const pageRepos = filteredRepositoryURLs.slice(
|
||||
computeStart(),
|
||||
computeEnd()
|
||||
);
|
||||
|
||||
// Filter to avoid adding duplicates
|
||||
const newSelected = [
|
||||
...pageRepos.filter((repoId) => !selected.includes(repoId)),
|
||||
];
|
||||
|
||||
updateSelected([...selected, ...newSelected]);
|
||||
};
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
updateSelected([]);
|
||||
};
|
||||
|
||||
return (
|
||||
(isError && <Error />) ||
|
||||
(isLoading && <Loading />) ||
|
||||
(isSuccess && (
|
||||
<>
|
||||
{Object.values(repositories).length === 0 ? (
|
||||
<Empty refetch={refetch} isFetching={isFetching} />
|
||||
) : (
|
||||
<>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem variant="bulk-select">
|
||||
<BulkSelect
|
||||
selected={selected}
|
||||
count={Object.values(repositories).length}
|
||||
filteredCount={filteredRepositoryURLs.length}
|
||||
perPage={perPage}
|
||||
handleSelectAll={handleSelectAll}
|
||||
handleSelectPage={handleSelectPage}
|
||||
handleDeselectAll={handleDeselectAll}
|
||||
isDisabled={isFetching}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem variant="search-filter">
|
||||
<SearchInput
|
||||
aria-label="Search repositories"
|
||||
onChange={handleFilterRepositories}
|
||||
value={filterValue}
|
||||
onClear={handleClearFilter}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
variant="primary"
|
||||
isInline
|
||||
onClick={() => refetch()}
|
||||
isLoading={isFetching}
|
||||
>
|
||||
{isFetching ? 'Refreshing' : 'Refresh'}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<ToggleGroup aria-label="Filter repositories list">
|
||||
<ToggleGroupItem
|
||||
text="All"
|
||||
aria-label="All repositories"
|
||||
buttonId="toggle-group-all"
|
||||
isSelected={toggleSelected === 'toggle-group-all'}
|
||||
onChange={handleToggleClick}
|
||||
/>
|
||||
<ToggleGroupItem
|
||||
text="Selected"
|
||||
aria-label="Selected repositories"
|
||||
buttonId="toggle-group-selected"
|
||||
isSelected={toggleSelected === 'toggle-group-selected'}
|
||||
onChange={handleToggleClick}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem variant="pagination">
|
||||
<Pagination
|
||||
itemCount={filteredRepositoryURLs.length}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={handleSetPage}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
isCompact
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<Panel>
|
||||
<PanelMain>
|
||||
<RepositoryUnavailable />
|
||||
<Table variant="compact" data-testid="repositories-table">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th aria-label="Selected" />
|
||||
<Th width={45}>Name</Th>
|
||||
<Th width={15}>Architecture</Th>
|
||||
<Th>Version</Th>
|
||||
<Th width={10}>Packages</Th>
|
||||
<Th>Status</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{filteredRepositoryURLs
|
||||
.sort((a, b) => {
|
||||
if (repositories[a].name < repositories[b].name) {
|
||||
return -1;
|
||||
} else if (
|
||||
repositories[b].name < repositories[a].name
|
||||
) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
})
|
||||
.slice(computeStart(), computeEnd())
|
||||
.map((repoURL, rowIndex) => {
|
||||
const repo = repositories[repoURL];
|
||||
const repoExists = repo.name ? true : false;
|
||||
return (
|
||||
<Tr key={repo.url}>
|
||||
<Td
|
||||
select={{
|
||||
isSelected: isRepoSelected(repo.url),
|
||||
rowIndex: rowIndex,
|
||||
onSelect: (event, isSelecting) =>
|
||||
handleSelect(repo.url, rowIndex, isSelecting),
|
||||
isDisabled:
|
||||
isFetching || repo.status !== 'Valid',
|
||||
}}
|
||||
/>
|
||||
<Td dataLabel={'Name'}>
|
||||
{repoExists
|
||||
? repo.name
|
||||
: 'Repository with the following url is no longer available:'}
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={repo.url}
|
||||
>
|
||||
{repo.url}
|
||||
</Button>
|
||||
</Td>
|
||||
<Td dataLabel={'Architecture'}>
|
||||
{repoExists ? repo.distribution_arch : '-'}
|
||||
</Td>
|
||||
<Td dataLabel={'Version'}>
|
||||
{repoExists ? repo.distribution_versions : '-'}
|
||||
</Td>
|
||||
<Td dataLabel={'Packages'}>
|
||||
{repoExists ? repo.package_count : '-'}
|
||||
</Td>
|
||||
<Td dataLabel={'Status'}>
|
||||
<RepositoriesStatus
|
||||
repoStatus={
|
||||
repoExists ? repo.status : 'Unavailable'
|
||||
}
|
||||
repoUrl={repo.url}
|
||||
repoIntrospections={
|
||||
repo.last_introspection_time
|
||||
}
|
||||
repoFailCount={repo.failed_introspections_count}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
<Pagination
|
||||
itemCount={filteredRepositoryURLs.length}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={handleSetPage}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
variant={PaginationVariant.bottom}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
))
|
||||
);
|
||||
};
|
||||
|
||||
const Error = () => {
|
||||
return (
|
||||
<Alert title="Repositories unavailable" variant="danger" isPlain isInline>
|
||||
Repositories cannot be reached, try again later.
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<EmptyState>
|
||||
<EmptyStateHeader
|
||||
titleText="Loading"
|
||||
icon={<EmptyStateIcon icon={Spinner} />}
|
||||
headingLevel="h4"
|
||||
/>
|
||||
</EmptyState>
|
||||
);
|
||||
};
|
||||
|
||||
const Empty = ({ isFetching, refetch }) => {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
return (
|
||||
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
|
||||
<EmptyStateHeader
|
||||
titleText="No Custom Repositories"
|
||||
icon={<EmptyStateIcon icon={RepositoryIcon} />}
|
||||
headingLevel="h4"
|
||||
/>
|
||||
<EmptyStateBody>
|
||||
Repositories can be added in the "Repositories" area of the
|
||||
console. Once added, refresh this page to see them.
|
||||
</EmptyStateBody>
|
||||
<EmptyStateFooter>
|
||||
<Button
|
||||
variant="primary"
|
||||
component="a"
|
||||
target="_blank"
|
||||
href={isBeta() ? '/preview/settings/content' : '/settings/content'}
|
||||
className="pf-u-mr-sm"
|
||||
>
|
||||
Go to repositories
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isInline
|
||||
onClick={() => refetch()}
|
||||
isLoading={isFetching}
|
||||
>
|
||||
{isFetching ? 'Refreshing' : 'Refresh'}
|
||||
</Button>
|
||||
</EmptyStateFooter>
|
||||
</EmptyState>
|
||||
);
|
||||
};
|
||||
|
||||
BulkSelect.propTypes = {
|
||||
selected: PropTypes.array,
|
||||
count: PropTypes.number,
|
||||
filteredCount: PropTypes.number,
|
||||
perPage: PropTypes.number,
|
||||
handleSelectAll: PropTypes.func,
|
||||
handleSelectPage: PropTypes.func,
|
||||
handleDeselectAll: PropTypes.func,
|
||||
isDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
Empty.propTypes = {
|
||||
isFetching: PropTypes.bool,
|
||||
refetch: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Repositories;
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
DescriptionList,
|
||||
DescriptionListDescription,
|
||||
DescriptionListGroup,
|
||||
DescriptionListTerm,
|
||||
Popover,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
ExternalLinkAltIcon,
|
||||
InProgressIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
import { ApiRepositoryResponse } from '../../../store/contentSourcesApi';
|
||||
import {
|
||||
convertStringToDate,
|
||||
timestampToDisplayString,
|
||||
} from '../../../Utilities/time';
|
||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
||||
|
||||
const getLastIntrospection = (
|
||||
repoIntrospections: RepositoryStatusProps['repoIntrospections']
|
||||
) => {
|
||||
const currentDate = Date.now();
|
||||
const lastIntrospectionDate = convertStringToDate(repoIntrospections);
|
||||
const timeDeltaInSeconds = Math.floor(
|
||||
(currentDate - lastIntrospectionDate) / 1000
|
||||
);
|
||||
|
||||
if (timeDeltaInSeconds <= 60) {
|
||||
return 'A few seconds ago';
|
||||
} else if (timeDeltaInSeconds <= 60 * 60) {
|
||||
return 'A few minutes ago';
|
||||
} else if (timeDeltaInSeconds <= 60 * 60 * 24) {
|
||||
return 'A few hours ago';
|
||||
} else {
|
||||
return timestampToDisplayString(repoIntrospections);
|
||||
}
|
||||
};
|
||||
|
||||
type RepositoryStatusProps = {
|
||||
repoStatus: ApiRepositoryResponse['status'];
|
||||
repoUrl: ApiRepositoryResponse['url'];
|
||||
repoIntrospections: ApiRepositoryResponse['last_introspection_time'];
|
||||
repoFailCount: ApiRepositoryResponse['failed_introspections_count'];
|
||||
};
|
||||
|
||||
const RepositoriesStatus = ({
|
||||
repoStatus,
|
||||
repoUrl,
|
||||
repoIntrospections,
|
||||
repoFailCount,
|
||||
}: RepositoryStatusProps) => {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
if (repoStatus === 'Valid') {
|
||||
return (
|
||||
<>
|
||||
<CheckCircleIcon className="success" /> {repoStatus}
|
||||
</>
|
||||
);
|
||||
} else if (repoStatus === 'Invalid' || repoStatus === 'Unavailable') {
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
position="bottom"
|
||||
minWidth="30rem"
|
||||
bodyContent={
|
||||
<>
|
||||
<Alert
|
||||
variant={repoStatus === 'Invalid' ? 'danger' : 'warning'}
|
||||
title={repoStatus}
|
||||
className="pf-u-pb-sm"
|
||||
isInline
|
||||
isPlain
|
||||
/>
|
||||
<p className="pf-u-pb-md">Cannot fetch {repoUrl}</p>
|
||||
{(repoIntrospections || repoFailCount) && (
|
||||
<>
|
||||
<DescriptionList
|
||||
columnModifier={{
|
||||
default: '2Col',
|
||||
}}
|
||||
>
|
||||
{repoIntrospections && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
Last introspection
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{getLastIntrospection(repoIntrospections)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
{repoFailCount && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
Failed attempts
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{repoFailCount}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
</DescriptionList>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
iconPosition="right"
|
||||
isInline
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
href={
|
||||
isBeta() ? '/preview/settings/content' : '/settings/content'
|
||||
}
|
||||
>
|
||||
Go to Repositories
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button variant="link" className="pf-u-p-0 pf-u-font-size-sm">
|
||||
{repoStatus === 'Invalid' && (
|
||||
<ExclamationCircleIcon className="error" />
|
||||
)}
|
||||
{repoStatus === 'Unavailable' && (
|
||||
<ExclamationTriangleIcon className="expiring" />
|
||||
)}{' '}
|
||||
<span className="failure-button">{repoStatus}</span>
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
} else if (repoStatus === 'Pending') {
|
||||
return (
|
||||
<>
|
||||
<InProgressIcon className="pending" /> {repoStatus}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RepositoriesStatus;
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert, Button } from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { useCheckRepositoriesAvailability } from '../../../Utilities/checkRepositoriesAvailability';
|
||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
||||
|
||||
const RepositoryUnavailable = () => {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
|
||||
if (useCheckRepositoriesAvailability()) {
|
||||
return (
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Previously added custom repository unavailable"
|
||||
isInline
|
||||
>
|
||||
A repository that was used to build this image previously is not
|
||||
available. Address the error found in the last introspection and
|
||||
validate that the repository is still accessible.
|
||||
<br />
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
iconPosition="right"
|
||||
isInline
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
href={isBeta() ? '/preview/settings/content' : '/settings/content'}
|
||||
>
|
||||
Go to Repositories
|
||||
</Button>
|
||||
</Alert>
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export default RepositoryUnavailable;
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import {
|
||||
ExpandableSection,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
|
||||
import OscapProfileInformation from './OscapProfileInformation';
|
||||
import RepositoryUnavailable from './RepositoryUnavailable';
|
||||
import {
|
||||
ContentList,
|
||||
FSCList,
|
||||
ImageDetailsList,
|
||||
ImageOutputList,
|
||||
RegisterLaterList,
|
||||
RegisterNowList,
|
||||
TargetEnvAWSList,
|
||||
TargetEnvAzureList,
|
||||
TargetEnvGCPList,
|
||||
TargetEnvOciList,
|
||||
TargetEnvOtherList,
|
||||
} from './ReviewStepTextLists';
|
||||
import UsrSubDirectoriesDisabled from './UsrSubDirectoriesDisabled';
|
||||
|
||||
import isRhel from '../../../Utilities/isRhel';
|
||||
|
||||
const ReviewStep = () => {
|
||||
const { auth } = useChrome();
|
||||
const [isExpandedImageOutput, setIsExpandedImageOutput] = useState(false);
|
||||
const [isExpandedTargetEnvs, setIsExpandedTargetEnvs] = useState(false);
|
||||
const [isExpandedFSC, setIsExpandedFSC] = useState(false);
|
||||
const [isExpandedContent, setIsExpandedContent] = useState(false);
|
||||
const [isExpandedRegistration, setIsExpandedRegistration] = useState(false);
|
||||
const [isExpandedImageDetail, setIsExpandedImageDetail] = useState(false);
|
||||
const [isExpandedOscapDetail, setIsExpandedOscapDetail] = useState(false);
|
||||
const { change, getState } = useFormApi();
|
||||
|
||||
useEffect(() => {
|
||||
const registerSystem = getState()?.values?.['register-system'];
|
||||
if (registerSystem?.startsWith('register-now')) {
|
||||
(async () => {
|
||||
const userData = await auth?.getUser();
|
||||
const id = userData?.identity?.internal?.org_id;
|
||||
change('subscription-organization-id', id);
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
const onToggleImageOutput = (isExpandedImageOutput) =>
|
||||
setIsExpandedImageOutput(isExpandedImageOutput);
|
||||
const onToggleTargetEnvs = (isExpandedTargetEnvs) =>
|
||||
setIsExpandedTargetEnvs(isExpandedTargetEnvs);
|
||||
const onToggleFSC = (isExpandedFSC) => setIsExpandedFSC(isExpandedFSC);
|
||||
const onToggleContent = (isExpandedContent) =>
|
||||
setIsExpandedContent(isExpandedContent);
|
||||
const onToggleRegistration = (isExpandedRegistration) =>
|
||||
setIsExpandedRegistration(isExpandedRegistration);
|
||||
const onToggleImageDetail = (isExpandedImageDetail) =>
|
||||
setIsExpandedImageDetail(isExpandedImageDetail);
|
||||
const onToggleOscapDetails = (isExpandedOscapDetail) =>
|
||||
setIsExpandedOscapDetail(isExpandedOscapDetail);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RepositoryUnavailable />
|
||||
{getState()?.values?.['file-system-configuration']?.find((mp) =>
|
||||
mp.mountpoint.includes('/usr')
|
||||
) && <UsrSubDirectoriesDisabled />}
|
||||
<ExpandableSection
|
||||
toggleContent={'Image output'}
|
||||
onToggle={(_event, isExpandedImageOutput) =>
|
||||
onToggleImageOutput(isExpandedImageOutput)
|
||||
}
|
||||
isExpanded={isExpandedImageOutput}
|
||||
isIndented
|
||||
data-testid="image-output-expandable"
|
||||
>
|
||||
<ImageOutputList />
|
||||
</ExpandableSection>
|
||||
<ExpandableSection
|
||||
toggleContent={'Target environments'}
|
||||
onToggle={(_event, isExpandedTargetEnvs) =>
|
||||
onToggleTargetEnvs(isExpandedTargetEnvs)
|
||||
}
|
||||
isExpanded={isExpandedTargetEnvs}
|
||||
isIndented
|
||||
data-testid="target-environments-expandable"
|
||||
>
|
||||
{getState()?.values?.['target-environment']?.aws && (
|
||||
<TargetEnvAWSList />
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.gcp && (
|
||||
<TargetEnvGCPList />
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.azure && (
|
||||
<TargetEnvAzureList />
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.oci && (
|
||||
<TargetEnvOciList />
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.vsphere && (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>VMware vSphere (.vmdk)</Text>
|
||||
<TargetEnvOtherList />
|
||||
</TextContent>
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.['vsphere-ova'] && (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>VMware vSphere (.ova)</Text>
|
||||
<TargetEnvOtherList />
|
||||
</TextContent>
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.['guest-image'] && (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>
|
||||
Virtualization - Guest image (.qcow2)
|
||||
</Text>
|
||||
<TargetEnvOtherList />
|
||||
</TextContent>
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.['image-installer'] && (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>
|
||||
Bare metal - Installer (.iso)
|
||||
</Text>
|
||||
<TargetEnvOtherList />
|
||||
</TextContent>
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.wsl && (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>
|
||||
WSL - Windows Subsystem for Linux (.tar.gz)
|
||||
</Text>
|
||||
<TargetEnvOtherList />
|
||||
</TextContent>
|
||||
)}
|
||||
</ExpandableSection>
|
||||
<ExpandableSection
|
||||
toggleContent={'File system configuration'}
|
||||
onToggle={(_event, isExpandedFSC) => onToggleFSC(isExpandedFSC)}
|
||||
isExpanded={isExpandedFSC}
|
||||
isIndented
|
||||
data-testid="file-system-configuration-expandable"
|
||||
>
|
||||
<FSCList />
|
||||
</ExpandableSection>
|
||||
<ExpandableSection
|
||||
toggleContent={'Content'}
|
||||
onToggle={(_event, isExpandedContent) =>
|
||||
onToggleContent(isExpandedContent)
|
||||
}
|
||||
isExpanded={isExpandedContent}
|
||||
isIndented
|
||||
data-testid="content-expandable"
|
||||
>
|
||||
<ContentList />
|
||||
</ExpandableSection>
|
||||
{isRhel(getState()?.values?.release) && (
|
||||
<ExpandableSection
|
||||
toggleContent={'Registration'}
|
||||
onToggle={(_event, isExpandedRegistration) =>
|
||||
onToggleRegistration(isExpandedRegistration)
|
||||
}
|
||||
isExpanded={isExpandedRegistration}
|
||||
isIndented
|
||||
data-testid="registration-expandable"
|
||||
>
|
||||
{getState()?.values?.['register-system'] === 'register-later' && (
|
||||
<RegisterLaterList />
|
||||
)}
|
||||
{getState()?.values?.['register-system']?.startsWith(
|
||||
'register-now'
|
||||
) && <RegisterNowList />}
|
||||
</ExpandableSection>
|
||||
)}
|
||||
{(getState()?.values?.['image-name'] ||
|
||||
getState()?.values?.['image-description']) && (
|
||||
<ExpandableSection
|
||||
toggleContent={'Image details'}
|
||||
onToggle={(_event, isExpandedImageDetail) =>
|
||||
onToggleImageDetail(isExpandedImageDetail)
|
||||
}
|
||||
isExpanded={isExpandedImageDetail}
|
||||
isIndented
|
||||
data-testid="image-details-expandable"
|
||||
>
|
||||
<ImageDetailsList />
|
||||
</ExpandableSection>
|
||||
)}
|
||||
{getState()?.values?.['oscap-profile'] && (
|
||||
<ExpandableSection
|
||||
toggleContent={'OpenSCAP'}
|
||||
onToggle={(_event, isExpandedOscapDetail) =>
|
||||
onToggleOscapDetails(isExpandedOscapDetail)
|
||||
}
|
||||
isExpanded={isExpandedOscapDetail}
|
||||
isIndented
|
||||
data-testid="oscap-detail-expandable"
|
||||
>
|
||||
<OscapProfileInformation />
|
||||
</ExpandableSection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReviewStep;
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useFormApi } from '@data-driven-forms/react-form-renderer';
|
||||
import { Alert, Panel, PanelMain, Spinner } from '@patternfly/react-core';
|
||||
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { UNIT_GIB, UNIT_MIB } from '../../../constants';
|
||||
import { useListRepositoriesQuery } from '../../../store/contentSourcesApi';
|
||||
|
||||
const RepoName = ({ repoUrl }) => {
|
||||
const { data, isSuccess, isFetching, isError } = useListRepositoriesQuery({
|
||||
url: repoUrl,
|
||||
contentType: 'rpm',
|
||||
origin: 'external',
|
||||
});
|
||||
|
||||
const errorLoading = () => {
|
||||
return (
|
||||
<Alert
|
||||
variant="danger"
|
||||
isInline
|
||||
isPlain
|
||||
title="Error loading repository name"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
this might be a tad bit hacky
|
||||
"isSuccess" indicates only that the query fetched successfuly, but it
|
||||
doesn't differentiate between a scenario when the repository was found
|
||||
in the response and when it was not
|
||||
for this reason I've split the "isSuccess" into two paths:
|
||||
- query finished and the repo was found -> render the name of the repo
|
||||
- query finished, but the repo was not found -> render an error
|
||||
*/}
|
||||
{isSuccess && data.data?.[0]?.name && <p>{data.data?.[0].name}</p>}
|
||||
{isSuccess && !data.data?.[0]?.name && errorLoading()}
|
||||
{isFetching && <Spinner size="md" />}
|
||||
{isError && errorLoading()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FSReviewTable = () => {
|
||||
const { getState } = useFormApi();
|
||||
const fsc = getState().values['file-system-configuration'];
|
||||
return (
|
||||
<Panel isScrollable>
|
||||
<PanelMain maxHeight="30ch">
|
||||
<Table aria-label="File system configuration table" variant="compact">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Mount point</Th>
|
||||
<Th>File system type</Th>
|
||||
<Th>Minimum size</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody data-testid="file-system-configuration-tbody-review">
|
||||
{fsc.map((partition, partitionIndex) => (
|
||||
<Tr key={partitionIndex}>
|
||||
<Td className="pf-m-width-30">{partition.mountpoint}</Td>
|
||||
<Td className="pf-m-width-30">xfs</Td>
|
||||
<Td className="pf-m-width-30">
|
||||
{partition.size}{' '}
|
||||
{partition.unit === UNIT_GIB
|
||||
? 'GiB'
|
||||
: partition.unit === UNIT_MIB
|
||||
? 'MiB'
|
||||
: 'KiB'}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
export const PackagesTable = () => {
|
||||
const { getState } = useFormApi();
|
||||
const packages = getState()?.values['selected-packages'];
|
||||
return (
|
||||
<Panel isScrollable>
|
||||
<PanelMain maxHeight="30ch">
|
||||
<Table aria-label="Packages table" variant="compact">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody data-testid="packages-tbody-review">
|
||||
{packages.map((pkg, pkgIndex) => (
|
||||
<Tr key={pkgIndex}>
|
||||
<Td className="pf-m-width-30">{pkg.name}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
export const RepositoriesTable = () => {
|
||||
const { getState } = useFormApi();
|
||||
const repositories = getState()?.values?.['payload-repositories'];
|
||||
return (
|
||||
<Panel isScrollable>
|
||||
<PanelMain maxHeight="30ch">
|
||||
<Table aria-label="Custom repositories table" variant="compact">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody data-testid="repositories-tbody-review">
|
||||
{repositories.map((repo, repoIndex) => (
|
||||
<Tr key={repoIndex}>
|
||||
<Td className="pf-m-width-60">
|
||||
<RepoName repoUrl={repo.baseurl} />
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
RepoName.propTypes = {
|
||||
repoUrl: PropTypes.string,
|
||||
};
|
||||
|
|
@ -1,616 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useFormApi } from '@data-driven-forms/react-form-renderer';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Popover,
|
||||
Spinner,
|
||||
Text,
|
||||
TextContent,
|
||||
TextList,
|
||||
TextListItem,
|
||||
TextListVariants,
|
||||
TextListItemVariants,
|
||||
TextVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExclamationTriangleIcon, HelpIcon } from '@patternfly/react-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ActivationKeyInformation from './ActivationKeyInformation';
|
||||
import { AwsAccountId } from './AwsAccountId';
|
||||
import ReleaseLifecycle from './ReleaseLifecycle';
|
||||
import {
|
||||
FSReviewTable,
|
||||
PackagesTable,
|
||||
RepositoriesTable,
|
||||
} from './ReviewStepTables';
|
||||
|
||||
import { RELEASES, UNIT_GIB } from '../../../constants';
|
||||
import { extractProvisioningList } from '../../../store/helpers';
|
||||
import { useGetSourceListQuery } from '../../../store/provisioningApi';
|
||||
import { useShowActivationKeyQuery } from '../../../store/rhsmApi';
|
||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
||||
import { googleAccType } from '../steps/googleCloud';
|
||||
|
||||
const ExpirationWarning = () => {
|
||||
return (
|
||||
<div className="pf-u-mr-sm pf-u-font-size-sm pf-u-warning-color-100">
|
||||
<ExclamationTriangleIcon /> Expires 14 days after creation
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ImageOutputList = () => {
|
||||
const { getState } = useFormApi();
|
||||
return (
|
||||
<TextContent>
|
||||
<ReleaseLifecycle />
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Release
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{RELEASES.get(getState()?.values?.release)}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Architecture
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.arch}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const TargetEnvAWSList = () => {
|
||||
const { data: rawAWSSources, isSuccess } = useGetSourceListQuery({
|
||||
provider: 'aws',
|
||||
});
|
||||
const awsSources = extractProvisioningList(rawAWSSources);
|
||||
const { isBeta } = useGetEnvironment();
|
||||
|
||||
const { getState } = useFormApi();
|
||||
return (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>AWS</Text>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Image type
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
Red Hat hosted image
|
||||
<br />
|
||||
<ExpirationWarning />
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Shared to account
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{!isBeta() && getState()?.values?.['aws-account-id']}
|
||||
{isBeta() &&
|
||||
getState()?.values?.['aws-target-type'] ===
|
||||
'aws-target-type-source' &&
|
||||
isSuccess && (
|
||||
<AwsAccountId
|
||||
sourceId={getState()?.values?.['aws-sources-select']}
|
||||
/>
|
||||
)}
|
||||
{isBeta() &&
|
||||
getState()?.values?.['aws-target-type'] ===
|
||||
'aws-target-type-account-id' &&
|
||||
getState()?.values?.['aws-account-id']}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
{getState()?.values?.['aws-target-type'] === 'aws-target-type-source'
|
||||
? 'Source'
|
||||
: null}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{isSuccess &&
|
||||
getState()?.values?.['aws-target-type'] === 'aws-target-type-source'
|
||||
? awsSources.find(
|
||||
(source) =>
|
||||
source.id === getState()?.values?.['aws-sources-select']
|
||||
)?.name
|
||||
: null}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Default region
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
us-east-1
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const TargetEnvGCPList = () => {
|
||||
const { getState } = useFormApi();
|
||||
return (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>GCP</Text>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Image type
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
Red Hat hosted image
|
||||
<br />
|
||||
<ExpirationWarning />
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Account type
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{googleAccType?.[getState()?.values?.['google-account-type']]}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
{googleAccType?.[getState()?.values?.['google-account-type']] ===
|
||||
'Domain'
|
||||
? 'Domain'
|
||||
: 'Principal'}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['google-email'] ||
|
||||
getState()?.values?.['google-domain']}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const TargetEnvAzureList = () => {
|
||||
const { getState } = useFormApi();
|
||||
const { data: rawAzureSources, isSuccess: isSuccessAzureSources } =
|
||||
useGetSourceListQuery({ provider: 'azure' });
|
||||
const azureSources = extractProvisioningList(rawAzureSources);
|
||||
return (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Microsoft Azure</Text>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Image type
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
Red Hat hosted image
|
||||
<br />
|
||||
<ExpirationWarning />
|
||||
</TextListItem>
|
||||
{getState()?.values?.['azure-type'] === 'azure-type-source' &&
|
||||
isSuccessAzureSources && (
|
||||
<>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Azure Source
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{
|
||||
azureSources.find(
|
||||
(source) =>
|
||||
source.id === getState()?.values?.['azure-sources-select']
|
||||
)?.name
|
||||
}
|
||||
</TextListItem>
|
||||
</>
|
||||
)}
|
||||
{getState()?.values?.['azure-type'] === 'azure-type-manual' && (
|
||||
<>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Azure Tenant ID
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['azure-tenant-id']}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Subscription ID
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['azure-subscription-id']}
|
||||
</TextListItem>
|
||||
</>
|
||||
)}
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Resource group
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['azure-resource-group']}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const TargetEnvOciList = () => {
|
||||
return (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Oracle Cloud Infrastructure</Text>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Object Storage URL
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
The URL for the built image will be ready to copy
|
||||
<br />
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const TargetEnvOtherList = () => {
|
||||
return (
|
||||
<>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Image type
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
Built image will be available for download
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FSCList = () => {
|
||||
const { getState } = useFormApi();
|
||||
const isManual =
|
||||
getState()?.values?.['file-system-config-radio'] === 'manual';
|
||||
const partitions = getState()?.values?.['file-system-configuration'];
|
||||
|
||||
return (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Configuration type
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dd}
|
||||
data-testid="partitioning-auto-manual"
|
||||
>
|
||||
{isManual ? 'Manual' : 'Automatic'}
|
||||
{isManual && (
|
||||
<>
|
||||
{' '}
|
||||
<Popover
|
||||
position="bottom"
|
||||
headerContent="Partitions"
|
||||
hasAutoWidth
|
||||
minWidth="30rem"
|
||||
bodyContent={<FSReviewTable />}
|
||||
>
|
||||
<Button
|
||||
data-testid="file-system-configuration-popover"
|
||||
variant="link"
|
||||
aria-label="File system configuration info"
|
||||
aria-describedby="file-system-configuration-info"
|
||||
className="pf-u-pt-0 pf-u-pb-0"
|
||||
>
|
||||
View partitions
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</TextListItem>
|
||||
{isManual && (
|
||||
<>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Image size (minimum)
|
||||
<Popover
|
||||
hasAutoWidth
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Image Builder may extend this size based on requirements,
|
||||
selected packages, and configurations.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="File system configuration info"
|
||||
aria-describedby="file-system-configuration-info"
|
||||
className="pf-c-form__group-label-help"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</TextListItem>
|
||||
<MinSize isManual={isManual} partitions={partitions} />
|
||||
</>
|
||||
)}
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const MinSize = ({ isManual, partitions }) => {
|
||||
let minSize = '';
|
||||
if (isManual && partitions) {
|
||||
let size = 0;
|
||||
for (const partition of partitions) {
|
||||
size += partition.size * partition.unit;
|
||||
}
|
||||
|
||||
size = (size / UNIT_GIB).toFixed(1);
|
||||
if (size < 1) {
|
||||
minSize = `Less than 1 GiB`;
|
||||
} else {
|
||||
minSize = `${size} GiB`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TextListItem component={TextListItemVariants.dd}> {minSize} </TextListItem>
|
||||
);
|
||||
};
|
||||
|
||||
MinSize.propTypes = {
|
||||
isManual: PropTypes.bool,
|
||||
partitions: PropTypes.arrayOf(PropTypes.object),
|
||||
};
|
||||
|
||||
export const ContentList = () => {
|
||||
const { getState } = useFormApi();
|
||||
return (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Additional Red Hat
|
||||
<br />
|
||||
and 3rd party packages
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dd}
|
||||
data-testid="chosen-packages-count"
|
||||
>
|
||||
{getState()?.values?.['selected-packages']?.length > 0 ? (
|
||||
<Popover
|
||||
position="bottom"
|
||||
headerContent="Additional packages"
|
||||
hasAutoWidth
|
||||
minWidth="30rem"
|
||||
bodyContent={<PackagesTable />}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
aria-label="About packages key"
|
||||
className="pf-u-p-0"
|
||||
>
|
||||
{getState()?.values?.['selected-packages']?.length}
|
||||
</Button>
|
||||
</Popover>
|
||||
) : (
|
||||
0
|
||||
)}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Custom repositories
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dd}
|
||||
data-testid="custom-repositories-count"
|
||||
>
|
||||
{getState()?.values?.['payload-repositories']?.length > 0 ? (
|
||||
<Popover
|
||||
position="bottom"
|
||||
headerContent="Custom repositories"
|
||||
hasAutoWidth
|
||||
minWidth="30rem"
|
||||
bodyContent={<RepositoriesTable />}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
aria-label="About custom repositories"
|
||||
className="pf-u-p-0"
|
||||
>
|
||||
{getState()?.values?.['payload-repositories']?.length || 0}
|
||||
</Button>
|
||||
</Popover>
|
||||
) : (
|
||||
0
|
||||
)}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const RegisterLaterList = () => {
|
||||
return (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Registration type
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
Register the system later
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const RegisterNowList = () => {
|
||||
const { getState } = useFormApi();
|
||||
const activationKey = getState()?.values?.['subscription-activation-key'];
|
||||
const { isError } = useShowActivationKeyQuery(
|
||||
{ name: activationKey },
|
||||
{
|
||||
skip: !activationKey,
|
||||
}
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Registration type
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dd}
|
||||
data-testid="review-registration"
|
||||
>
|
||||
<TextList isPlain>
|
||||
{getState()?.values?.['register-system']?.startsWith(
|
||||
'register-now'
|
||||
) && (
|
||||
<TextListItem>
|
||||
Register with Red Hat Subscription Manager (RHSM)
|
||||
<br />
|
||||
</TextListItem>
|
||||
)}
|
||||
{(getState()?.values?.['register-system'] ===
|
||||
'register-now-insights' ||
|
||||
getState()?.values?.['register-system'] ===
|
||||
'register-now-rhc') && (
|
||||
<TextListItem>
|
||||
Connect to Red Hat Insights
|
||||
<br />
|
||||
</TextListItem>
|
||||
)}
|
||||
{getState()?.values?.['register-system'] ===
|
||||
'register-now-rhc' && (
|
||||
<TextListItem>
|
||||
Use remote host configuration (rhc) utility
|
||||
<br />
|
||||
</TextListItem>
|
||||
)}
|
||||
</TextList>
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Activation key
|
||||
<Popover
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Activation keys enable you to register a system with
|
||||
appropriate subscriptions, system purpose, and repositories
|
||||
attached.
|
||||
<br />
|
||||
<br />
|
||||
If using an activation key with command line registration,
|
||||
you must provide your organization's ID. Your
|
||||
organization's ID is{' '}
|
||||
{getState()?.values?.['subscription-organization-id'] !==
|
||||
undefined ? (
|
||||
getState()?.values?.['subscription-organization-id']
|
||||
) : (
|
||||
<Spinner size="md" />
|
||||
)}
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="About activation key"
|
||||
className="pf-u-pl-sm pf-u-pt-0 pf-u-pb-0"
|
||||
size="sm"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
<ActivationKeyInformation />
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
{isError && (
|
||||
<Alert
|
||||
title="Information about the activation key unavailable"
|
||||
variant="danger"
|
||||
isPlain
|
||||
isInline
|
||||
>
|
||||
Information about the activation key cannot be loaded. Please check
|
||||
the key was not removed and try again later.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ImageDetailsList = () => {
|
||||
const { getState } = useFormApi();
|
||||
const imageName = getState()?.values?.['image-name'];
|
||||
const imageDescription = getState()?.values?.['image-description'];
|
||||
|
||||
return (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
{imageName && (
|
||||
<>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Image name
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{imageName}
|
||||
</TextListItem>
|
||||
</>
|
||||
)}
|
||||
{imageDescription && (
|
||||
<>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Description
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{imageDescription}
|
||||
</TextListItem>
|
||||
</>
|
||||
)}
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Grid, GridItem, TextInput } from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { UNIT_GIB, UNIT_KIB, UNIT_MIB } 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);
|
||||
|
||||
useEffect(() => {
|
||||
props.onChange(size, unit);
|
||||
}, [unit, size]);
|
||||
|
||||
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;
|
||||
// no default
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
// TODO make these stack vertically for xs viewport
|
||||
<Grid>
|
||||
<GridItem span={6}>
|
||||
<TextInput
|
||||
ouiaId="size"
|
||||
type="text"
|
||||
value={size}
|
||||
aria-label="Size text input"
|
||||
onChange={(_event, v) =>
|
||||
setSize(isNaN(parseInt(v)) ? 0 : parseInt(v))
|
||||
}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem span={6}>
|
||||
<Select
|
||||
ouiaId="unit"
|
||||
isOpen={isOpen}
|
||||
onToggle={(_event, isOpen) => onToggle(isOpen)}
|
||||
onSelect={onSelect}
|
||||
selections={
|
||||
unit === UNIT_KIB ? 'KiB' : unit === UNIT_MIB ? 'MiB' : 'GiB'
|
||||
}
|
||||
variant={SelectVariant.single}
|
||||
aria-label="Unit select"
|
||||
>
|
||||
{['KiB', 'MiB', 'GiB'].map((u, index) => {
|
||||
return <SelectOption key={index} value={u} />;
|
||||
})}
|
||||
</Select>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
SizeUnit.propTypes = {
|
||||
size: PropTypes.number.isRequired,
|
||||
unit: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SizeUnit;
|
||||
|
|
@ -1,401 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import {
|
||||
Alert,
|
||||
Bullseye,
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
Popover,
|
||||
Radio,
|
||||
Spinner,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
Tile,
|
||||
} from '@patternfly/react-core';
|
||||
import { HelpIcon } from '@patternfly/react-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useField } from 'react-final-form';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { useGetArchitecturesQuery } from '../../../store/imageBuilderApi';
|
||||
import { provisioningApi } from '../../../store/provisioningApi';
|
||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
||||
|
||||
const useGetAllowedTargets = ({ architecture, release }) => {
|
||||
const { data, isFetching, isSuccess, isError } = useGetArchitecturesQuery({
|
||||
distribution: release,
|
||||
});
|
||||
|
||||
let image_types = [];
|
||||
if (isSuccess && data) {
|
||||
data.forEach((elem) => {
|
||||
if (elem.arch === architecture) {
|
||||
image_types = elem.image_types;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: image_types,
|
||||
isFetching: isFetching,
|
||||
isSuccess: isSuccess,
|
||||
isError: isError,
|
||||
};
|
||||
};
|
||||
|
||||
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,
|
||||
oci: false,
|
||||
'vsphere-ova': false,
|
||||
vsphere: false,
|
||||
'guest-image': false,
|
||||
'image-installer': false,
|
||||
wsl: false,
|
||||
});
|
||||
const prefetchSources = provisioningApi.usePrefetch('getSourceList');
|
||||
const { isBeta } = useGetEnvironment();
|
||||
const release = getState()?.values?.release;
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Set the target via search parameter
|
||||
// Used by Insights assistant or external hyperlinks (access.redhat.com, developers.redhat.com)
|
||||
const preloadTarget = searchParams.get('target');
|
||||
useEffect(() => {
|
||||
preloadTarget === 'iso' && handleSetEnvironment('image-installer', true);
|
||||
preloadTarget === 'qcow2' && handleSetEnvironment('guest-image', true);
|
||||
}, [preloadTarget]);
|
||||
|
||||
useEffect(() => {
|
||||
if (getState()?.values?.[input.name]) {
|
||||
setEnvironment(getState().values[input.name]);
|
||||
}
|
||||
}, [getState, input.name]);
|
||||
|
||||
const handleSetEnvironment = (env, checked) =>
|
||||
setEnvironment((prevEnv) => {
|
||||
const newEnv = {
|
||||
...prevEnv,
|
||||
[env]: checked,
|
||||
};
|
||||
change(input.name, newEnv);
|
||||
return newEnv;
|
||||
});
|
||||
|
||||
const handleKeyDown = (e, env, checked) => {
|
||||
if (e.key === ' ') {
|
||||
handleSetEnvironment(env, checked);
|
||||
}
|
||||
};
|
||||
|
||||
// Load all the allowed targets from the backend
|
||||
const architecture = useField('arch').input.value;
|
||||
|
||||
const {
|
||||
data: allowedTargets,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
} = useGetAllowedTargets({
|
||||
architecture: architecture,
|
||||
release: release,
|
||||
});
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<Bullseye>
|
||||
<Spinner size="lg" />
|
||||
</Bullseye>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !isSuccess) {
|
||||
return (
|
||||
<Alert
|
||||
variant={'danger'}
|
||||
isPlain
|
||||
isInline
|
||||
title={'Allowed targets unavailable'}
|
||||
>
|
||||
Allowed targets cannot be reached, try again later.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
// If the user already made a choice for some targets but then changes their
|
||||
// architecture or distribution, only keep the target choices that are still
|
||||
// compatible.
|
||||
const allTargets = [
|
||||
'aws',
|
||||
'gcp',
|
||||
'azure',
|
||||
'vsphere',
|
||||
'vsphere-ova',
|
||||
'guest-image',
|
||||
'image-installer',
|
||||
'wsl',
|
||||
];
|
||||
allTargets.forEach((target) => {
|
||||
if (environment[target] && !allowedTargets.includes(target)) {
|
||||
handleSetEnvironment(target, false);
|
||||
}
|
||||
});
|
||||
|
||||
// each item the user can select is depending on what's compatible with the
|
||||
// architecture and the distribution they previously selected
|
||||
return (
|
||||
<FormGroup
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
data-testid="target-select"
|
||||
>
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Public cloud</Text>}
|
||||
data-testid="target-public"
|
||||
>
|
||||
<div className="tiles">
|
||||
{allowedTargets.includes('aws') && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-aws"
|
||||
title="Amazon Web Services"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={'/apps/frontend-assets/partners-icons/aws.svg'}
|
||||
alt="Amazon Web Services logo"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('aws', !environment.aws)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'aws', !environment.aws)}
|
||||
onMouseEnter={() => prefetchSources({ provider: 'aws' })}
|
||||
isSelected={environment.aws}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('gcp') && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-google"
|
||||
title="Google Cloud Platform"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={
|
||||
'/apps/frontend-assets/partners-icons/google-cloud-short.svg'
|
||||
}
|
||||
alt="Google Cloud Platform logo"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('gcp', !environment.gcp)}
|
||||
isSelected={environment.gcp}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'gcp', !environment.gcp)}
|
||||
onMouseEnter={() => prefetchSources({ provider: 'gcp' })}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('azure') && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-azure"
|
||||
title="Microsoft Azure"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={
|
||||
'/apps/frontend-assets/partners-icons/microsoft-azure-short.svg'
|
||||
}
|
||||
alt="Microsoft Azure logo"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('azure', !environment.azure)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'azure', !environment.azure)}
|
||||
onMouseEnter={() => prefetchSources({ provider: 'azure' })}
|
||||
isSelected={environment.azure}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('oci') && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-oci"
|
||||
title="Oracle Cloud Infrastructure"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={'/apps/frontend-assets/partners-icons/oracle-short.svg'}
|
||||
alt="Oracle Cloud Infrastructure logo"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('oci', !environment.oci)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'oci', !environment.oci)}
|
||||
isSelected={environment.oci}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FormGroup>
|
||||
{allowedTargets.includes('vsphere') && (
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Private cloud</Text>}
|
||||
className="pf-u-mt-sm"
|
||||
data-testid="target-private"
|
||||
>
|
||||
<Checkbox
|
||||
label="VMware vSphere"
|
||||
isChecked={environment.vsphere || environment['vsphere-ova']}
|
||||
onChange={(_event, checked) => {
|
||||
handleSetEnvironment('vsphere-ova', checked);
|
||||
handleSetEnvironment('vsphere', false);
|
||||
}}
|
||||
aria-label="VMware checkbox"
|
||||
id="checkbox-vmware"
|
||||
name="VMware"
|
||||
data-testid="checkbox-vmware"
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{allowedTargets.includes('vsphere') && (
|
||||
<FormGroup
|
||||
className="pf-u-mt-sm pf-u-mb-sm pf-u-ml-xl"
|
||||
data-testid="target-private-vsphere-radio"
|
||||
>
|
||||
{allowedTargets.includes('vsphere-ova') && (
|
||||
<Radio
|
||||
name="vsphere-radio"
|
||||
aria-label="VMware vSphere radio button OVA"
|
||||
id="vsphere-radio-ova"
|
||||
label={
|
||||
<>
|
||||
Open virtualization format (.ova)
|
||||
<Popover
|
||||
maxWidth="30rem"
|
||||
position="right"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
An OVA file is a virtual appliance used by
|
||||
virtualization platforms such as VMware vSphere. It is
|
||||
a package that contains files used to describe a
|
||||
virtual machine, which includes a VMDK image, OVF
|
||||
descriptor file and a manifest file.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<HelpIcon className="pf-u-ml-sm" />
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
onChange={(_event, checked) => {
|
||||
handleSetEnvironment('vsphere-ova', checked);
|
||||
handleSetEnvironment('vsphere', !checked);
|
||||
}}
|
||||
isChecked={environment['vsphere-ova']}
|
||||
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
|
||||
/>
|
||||
)}
|
||||
<Radio
|
||||
className="pf-u-mt-sm"
|
||||
name="vsphere-radio"
|
||||
aria-label="VMware vSphere radio button VMDK"
|
||||
id="vsphere-radio-vmdk"
|
||||
label={
|
||||
<>
|
||||
Virtual disk (.vmdk)
|
||||
<Popover
|
||||
maxWidth="30rem"
|
||||
position="right"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
A VMDK file is a virtual disk that stores the contents
|
||||
of a virtual machine. This disk has to be imported into
|
||||
vSphere using govc import.vmdk, use the OVA version when
|
||||
using the vSphere UI.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<HelpIcon className="pf-u-ml-sm" />
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
onChange={(_event, checked) => {
|
||||
handleSetEnvironment('vsphere-ova', !checked);
|
||||
handleSetEnvironment('vsphere', checked);
|
||||
}}
|
||||
isChecked={environment.vsphere}
|
||||
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Other</Text>}
|
||||
data-testid="target-other"
|
||||
>
|
||||
{allowedTargets.includes('guest-image') && (
|
||||
<Checkbox
|
||||
label="Virtualization - Guest image (.qcow2)"
|
||||
isChecked={environment['guest-image']}
|
||||
onChange={(_event, checked) =>
|
||||
handleSetEnvironment('guest-image', checked)
|
||||
}
|
||||
aria-label="Virtualization guest image checkbox"
|
||||
id="checkbox-guest-image"
|
||||
name="Virtualization guest image"
|
||||
data-testid="checkbox-guest-image"
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('image-installer') && (
|
||||
<Checkbox
|
||||
label="Bare metal - Installer (.iso)"
|
||||
isChecked={environment['image-installer']}
|
||||
onChange={(_event, checked) =>
|
||||
handleSetEnvironment('image-installer', checked)
|
||||
}
|
||||
aria-label="Bare metal installer checkbox"
|
||||
id="checkbox-image-installer"
|
||||
name="Bare metal installer"
|
||||
data-testid="checkbox-image-installer"
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('wsl') && isBeta() && (
|
||||
<Checkbox
|
||||
label="WSL - Windows Subsystem for Linux (.tar.gz)"
|
||||
isChecked={environment['wsl']}
|
||||
onChange={(_event, checked) => handleSetEnvironment('wsl', checked)}
|
||||
aria-label="windows subsystem for linux checkbox"
|
||||
id="checkbox-wsl"
|
||||
name="WSL"
|
||||
data-testid="checkbox-wsl"
|
||||
/>
|
||||
)}
|
||||
</FormGroup>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
TargetEnvironment.propTypes = {
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
||||
TargetEnvironment.defaultProps = {
|
||||
label: '',
|
||||
isRequired: false,
|
||||
};
|
||||
|
||||
export default TargetEnvironment;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert } from '@patternfly/react-core';
|
||||
|
||||
const UsrSubDirectoriesDisabled = () => {
|
||||
return (
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Sub-directories for the /usr mount point are no longer supported"
|
||||
isInline
|
||||
>
|
||||
Please note that including sub-directories in the /usr path is no longer
|
||||
supported. Previously included mount points with /usr sub-directory are
|
||||
replaced by /usr when recreating an image.
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsrSubDirectoriesDisabled;
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
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,
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import nextStepMapper from './imageOutputStepMapper';
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import { DEFAULT_AWS_REGION } from '../../../constants';
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
const SourcesButton = () => {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={'settings/sources'}
|
||||
>
|
||||
Create and manage sources here
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const awsStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-target-aws',
|
||||
title: 'Amazon Web Services',
|
||||
customTitle: (
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Target environment - Amazon Web Services
|
||||
</Title>
|
||||
),
|
||||
name: 'aws-target-env',
|
||||
substepOf: 'Target environment',
|
||||
nextStep: ({ values }) => nextStepMapper(values, { skipAws: true }),
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'plain-text-component',
|
||||
label: (
|
||||
<p>
|
||||
Your image will be uploaded to AWS and shared with the account you
|
||||
provide below.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'plain-text-component',
|
||||
label: (
|
||||
<p>
|
||||
<b>The shared image will expire within 14 days.</b> To permanently
|
||||
access the image, copy the image, which will be shared to your account
|
||||
by Red Hat, to your own AWS account.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.RADIO,
|
||||
label: 'Share method:',
|
||||
name: 'aws-target-type',
|
||||
initialValue: 'aws-target-type-source',
|
||||
autoFocus: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Use an account configured from Sources.',
|
||||
description:
|
||||
'Use a configured source to launch environments directly from the console.',
|
||||
value: 'aws-target-type-source',
|
||||
'data-testid': 'aws-radio-source',
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
label: 'Manually enter an account ID.',
|
||||
value: 'aws-target-type-account-id',
|
||||
'data-testid': 'aws-radio-account-id',
|
||||
className: 'pf-u-mt-sm',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'aws-sources-select',
|
||||
name: 'aws-sources-select',
|
||||
className: 'pf-u-max-width',
|
||||
label: 'Source Name',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'aws-target-type',
|
||||
is: 'aws-target-type-source',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'aws-sources-select-description',
|
||||
label: <SourcesButton />,
|
||||
condition: {
|
||||
when: 'aws-target-type',
|
||||
is: 'aws-target-type-source',
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
type: validatorTypes.EXACT_LENGTH,
|
||||
threshold: 12,
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'aws-target-type',
|
||||
is: 'aws-target-type-account-id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gallery-layout',
|
||||
component: 'gallery-layout',
|
||||
minWidths: { default: '12.5rem' },
|
||||
maxWidths: { default: '12.5rem' },
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'aws-default-region',
|
||||
value: DEFAULT_AWS_REGION,
|
||||
'data-testid': 'aws-default-region',
|
||||
type: 'text',
|
||||
label: 'Default Region',
|
||||
isReadOnly: true,
|
||||
isRequired: true,
|
||||
helperText: (
|
||||
<HelperText>
|
||||
<HelperTextItem component="div" variant="indeterminate">
|
||||
Images are built in the default region but can be copied to
|
||||
other regions later.
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'aws-associated-account-id',
|
||||
'data-testid': 'aws-associated-account-id',
|
||||
type: 'text',
|
||||
label: 'Associated Account ID',
|
||||
isReadOnly: true,
|
||||
isRequired: true,
|
||||
helperText: (
|
||||
<HelperText>
|
||||
<HelperTextItem component="div" variant="indeterminate">
|
||||
This is the account associated with the source.
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
),
|
||||
condition: {
|
||||
when: 'aws-target-type',
|
||||
is: 'aws-target-type-source',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default awsStep;
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
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,
|
||||
Label,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import { FILE_SYSTEM_CUSTOMIZATION_URL } from '../../../constants';
|
||||
import FileSystemConfigButtons from '../formComponents/FileSystemConfigButtons';
|
||||
|
||||
export const reinitFileSystemConfiguratioStep = (change) => {
|
||||
change('file-system-configuration', undefined);
|
||||
change('file-system-config-radio', 'automatic');
|
||||
};
|
||||
|
||||
const fileSystemConfigurationStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-systemconfiguration-filesystem',
|
||||
title: 'File system configuration',
|
||||
name: 'File system configuration',
|
||||
buttons: FileSystemConfigButtons,
|
||||
nextStep: 'packages',
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'file-system-configuration-text-component',
|
||||
label: (
|
||||
<>
|
||||
<Text>Define the partitioning of the image</Text>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.RADIO,
|
||||
name: 'file-system-config-radio',
|
||||
initialValue: 'automatic',
|
||||
options: [
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<Text>
|
||||
<Label isCompact color="blue">
|
||||
Recommended
|
||||
</Label>{' '}
|
||||
Use automatic partitioning
|
||||
</Text>
|
||||
</>
|
||||
),
|
||||
description:
|
||||
'Automatically partition your image to what is best, depending on the target environment(s)',
|
||||
value: 'automatic',
|
||||
'data-testid': 'file-system-config-radio-automatic',
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
label: 'Manually configure partitions',
|
||||
description:
|
||||
'Manually configure the file system of your image by adding, removing, and editing partitions',
|
||||
value: 'manual',
|
||||
'data-testid': 'file-system-config-radio-manual',
|
||||
className: 'pf-u-mt-sm',
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'oscap-profile',
|
||||
is: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'file-system-configuration',
|
||||
name: 'file-system-configuration',
|
||||
label: 'File system configurations',
|
||||
validate: [
|
||||
{ type: 'fileSystemConfigurationValidator' },
|
||||
{ type: validatorTypes.REQUIRED },
|
||||
],
|
||||
condition: {
|
||||
or: [
|
||||
{
|
||||
when: 'file-system-config-radio',
|
||||
is: 'manual',
|
||||
},
|
||||
{ not: [{ when: 'oscap-profile', is: undefined }] },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'automatic-partitioning-info',
|
||||
label: (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Automatic partitioning</Text>
|
||||
<Text>
|
||||
Red Hat will automatically partition your image to what is best,
|
||||
depending on the target environment(s).
|
||||
</Text>
|
||||
<Text>
|
||||
The target environment sometimes dictates the partitioning scheme or
|
||||
parts of it, and sometimes the target environment is unknown (e.g.,
|
||||
for the .qcow2 generic cloud image).
|
||||
</Text>
|
||||
<Text>
|
||||
Using automatic partitioning will apply the most current supported
|
||||
configuration.
|
||||
<br></br>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
href={FILE_SYSTEM_CUSTOMIZATION_URL}
|
||||
className="pf-u-pl-0"
|
||||
>
|
||||
Customizing file systems during the image creation
|
||||
</Button>
|
||||
</Text>
|
||||
</TextContent>
|
||||
),
|
||||
condition: {
|
||||
when: 'file-system-config-radio',
|
||||
is: 'automatic',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default fileSystemConfigurationStep;
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
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,
|
||||
TextList,
|
||||
TextListItem,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import { HelpIcon } from '@patternfly/react-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import nextStepMapper from './imageOutputStepMapper';
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
export const googleAccType = {
|
||||
googleAccount: 'Google account',
|
||||
serviceAccount: 'Service account',
|
||||
googleGroup: 'Google group',
|
||||
domain: 'Domain',
|
||||
};
|
||||
|
||||
const PopoverInfo = ({ appendTo }) => {
|
||||
return (
|
||||
<Popover
|
||||
appendTo={appendTo}
|
||||
hasAutoWidth
|
||||
maxWidth="35rem"
|
||||
headerContent={'Valid account types'}
|
||||
flipBehavior={['right', 'bottom', 'top', 'left']}
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
The following account types can have an image shared with them:
|
||||
</Text>
|
||||
<TextList className="pf-u-ml-0">
|
||||
<TextListItem>
|
||||
<strong>Google account:</strong> A Google account represents a
|
||||
developer, an administrator, or any other person who interacts
|
||||
with Google Cloud. For example: <em>`alice@gmail.com`</em>.
|
||||
</TextListItem>
|
||||
<TextListItem>
|
||||
<strong>Service account:</strong> A service account is an account
|
||||
for an application instead of an individual end user. For example:{' '}
|
||||
<em>`myapp@appspot.gserviceaccount.com`</em>.
|
||||
</TextListItem>
|
||||
<TextListItem>
|
||||
<strong>Google group:</strong> A Google group is a named
|
||||
collection of Google accounts and service accounts. For example:{' '}
|
||||
<em>`admins@example.com`</em>.
|
||||
</TextListItem>
|
||||
<TextListItem>
|
||||
<strong>Google Workspace domain or Cloud Identity domain:</strong>{' '}
|
||||
A Google workspace or cloud identity domain represents a virtual
|
||||
group of all the Google accounts in an organization. These domains
|
||||
represent your organization's internet domain name. For
|
||||
example: <em>`mycompany.com`</em>.
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="Account info"
|
||||
aria-describedby="google-account-type"
|
||||
className="pf-c-form__group-label-help"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
PopoverInfo.propTypes = {
|
||||
appendTo: PropTypes.any,
|
||||
};
|
||||
|
||||
const googleCloudStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-target-gcp',
|
||||
title: 'Google Cloud Platform',
|
||||
customTitle: (
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Target environment - Google Cloud Platform
|
||||
</Title>
|
||||
),
|
||||
name: 'google-cloud-target-env',
|
||||
substepOf: 'Target environment',
|
||||
nextStep: ({ values }) =>
|
||||
nextStepMapper(values, { skipGoogle: true, skipAws: true }),
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'google-cloud-text-component',
|
||||
label: (
|
||||
<p>
|
||||
Select how to share your image. The image you create can be used to
|
||||
launch instances on GCP, regardless of which method you select.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.RADIO,
|
||||
label: 'Select image sharing',
|
||||
isRequired: true,
|
||||
name: 'image-sharing',
|
||||
initialValue: 'gcp-account',
|
||||
autoFocus: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Share image with a Google account',
|
||||
'data-testid': 'account-sharing',
|
||||
autoFocus: true,
|
||||
description: (
|
||||
<p>
|
||||
Your image will be uploaded to GCP and shared with the account you
|
||||
provide below.
|
||||
<b>The image expires in 14 days.</b> To keep permanent access to
|
||||
your image, copy it to your GCP project.
|
||||
</p>
|
||||
),
|
||||
value: 'gcp-account',
|
||||
},
|
||||
{
|
||||
label: 'Share image with Red Hat Insights only',
|
||||
'data-testid': 'insights-only-sharing',
|
||||
description: (
|
||||
<p>
|
||||
Your image will be uploaded to GCP and shared with Red Hat
|
||||
Insights.
|
||||
<b> The image expires in 14 days.</b> You cannot access or
|
||||
recreate this image in your GCP project.
|
||||
</p>
|
||||
),
|
||||
value: 'insights',
|
||||
autoFocus: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'radio-popover',
|
||||
label: 'Account 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,
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'image-sharing',
|
||||
is: 'gcp-account',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'google-email',
|
||||
'data-testid': 'input-google-email',
|
||||
type: 'text',
|
||||
label: 'Principal (e.g. e-mail address)',
|
||||
condition: {
|
||||
and: [
|
||||
{ when: 'image-sharing', is: 'gcp-account' },
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'google-domain',
|
||||
type: 'text',
|
||||
label: 'Domain',
|
||||
condition: {
|
||||
and: [
|
||||
{ when: 'image-sharing', is: 'gcp-account' },
|
||||
{ when: 'google-account-type', is: 'domain' },
|
||||
],
|
||||
},
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default googleCloudStep;
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useFormApi } from '@data-driven-forms/react-form-renderer';
|
||||
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
|
||||
import validatorTypes from '@data-driven-forms/react-form-renderer/validator-types';
|
||||
import { Flex, FlexItem, Text } from '@patternfly/react-core';
|
||||
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
const CharacterCount = () => {
|
||||
const { getState } = useFormApi();
|
||||
const description = getState().values?.['image-description'];
|
||||
return <h1>{description?.length || 0}/250</h1>;
|
||||
};
|
||||
|
||||
const imageNameStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-details',
|
||||
name: 'details',
|
||||
title: 'Details',
|
||||
nextStep: 'review',
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'plain-text-component',
|
||||
label: (
|
||||
<p>
|
||||
Optionally enter a name to identify your image later quickly. If you
|
||||
do not provide one, the UUID will be used as the name.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'image-name',
|
||||
type: 'text',
|
||||
label: 'Image Name',
|
||||
placeholder: 'Image Name',
|
||||
helperText:
|
||||
'The image name can be 3-63 characters long. It can contain lowercase letters, digits and hyphens, has to start with a letter and cannot end with a hyphen.',
|
||||
autoFocus: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.PATTERN,
|
||||
pattern: /^[a-z][-a-z0-9]{1,61}[a-z0-9]$/,
|
||||
message:
|
||||
'The image name can be 3-63 characters long. It can contain lowercase letters, digits and hyphens, has to start with a letter and cannot end with a hyphen.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXTAREA,
|
||||
name: 'image-description',
|
||||
type: 'text',
|
||||
label: (
|
||||
<Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
|
||||
<FlexItem>
|
||||
<Text component={'b'}>Description</Text>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<CharacterCount />
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
),
|
||||
placeholder: 'Add Description',
|
||||
resizeOrientation: 'vertical',
|
||||
validate: [{ type: validatorTypes.MAX_LENGTH, threshold: 250 }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default imageNameStep;
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
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 { Text } from '@patternfly/react-core';
|
||||
|
||||
import nextStepMapper from './imageOutputStepMapper';
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import { RHEL_9, X86_64 } from '../../../constants';
|
||||
import DocumentationButton from '../../sharedComponents/DocumentationButton';
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
const imageOutputStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-imageoutput',
|
||||
title: 'Image output',
|
||||
name: 'image-output',
|
||||
nextStep: ({ values }) => nextStepMapper(values),
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'image-output-plain-text',
|
||||
label: (
|
||||
<Text>
|
||||
Image builder allows you to create a custom image and push it to
|
||||
target environments.
|
||||
<br />
|
||||
<DocumentationButton />
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'image-output-release-select',
|
||||
label: 'Release',
|
||||
name: 'release',
|
||||
initialValue: RHEL_9,
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'image-output-release-lifecycle',
|
||||
label: 'Release lifecycle',
|
||||
name: 'release-lifecycle',
|
||||
},
|
||||
{
|
||||
component: 'image-output-arch-select',
|
||||
label: 'Architecture',
|
||||
name: 'arch',
|
||||
initialValue: X86_64,
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'centos-acknowledgement',
|
||||
name: 'centos-acknowledgement',
|
||||
condition: {
|
||||
when: 'release',
|
||||
pattern: /centos-*/,
|
||||
then: { set: { 'register-system': null } },
|
||||
else: { visible: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'output',
|
||||
name: 'target-environment',
|
||||
label: 'Select target environments',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
type: 'targetEnvironmentValidator',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default imageOutputStep;
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import isRhel from '../../../Utilities/isRhel';
|
||||
|
||||
const imageOutputStepMapper = (
|
||||
{ 'target-environment': targetEnv, release, enableOscap } = {},
|
||||
{ skipAws, skipGoogle, skipAzure } = {}
|
||||
) => {
|
||||
if (!skipAws && targetEnv?.aws) {
|
||||
return 'aws-target-env';
|
||||
}
|
||||
|
||||
if (!skipGoogle && targetEnv?.gcp) {
|
||||
return 'google-cloud-target-env';
|
||||
}
|
||||
|
||||
if (!skipAzure && targetEnv?.azure) {
|
||||
return 'ms-azure-target-env';
|
||||
}
|
||||
|
||||
if (isRhel(release)) {
|
||||
return 'registration';
|
||||
}
|
||||
if (enableOscap) {
|
||||
return 'Compliance';
|
||||
}
|
||||
return 'File system configuration';
|
||||
};
|
||||
|
||||
export default imageOutputStepMapper;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
export { default as awsTarget } from './aws';
|
||||
export { default as googleCloudTarget } from './googleCloud';
|
||||
export { default as msAzureTarget } from './msAzure';
|
||||
export { default as oscap } from './oscap';
|
||||
export { default as packages } from './packages';
|
||||
export { default as packagesContentSources } from './packagesContentSources';
|
||||
export { default as registration } from './registration';
|
||||
export { default as repositories } from './repositories';
|
||||
export { default as review } from './review';
|
||||
export { default as imageOutput } from './imageOutput';
|
||||
export { default as nextStepMapper } from './imageOutputStepMapper';
|
||||
export { default as fileSystemConfiguration } from './fileSystemConfiguration';
|
||||
export { default as imageName } from './imageName';
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
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, Text, TextContent, Title } from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import nextStepMapper from './imageOutputStepMapper';
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import { AZURE_AUTH_URL } from '../../../constants';
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
const SourcesButton = () => {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={'settings/sources'}
|
||||
>
|
||||
Create and manage sources here
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const msAzureStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-target-msazure',
|
||||
title: 'Microsoft Azure',
|
||||
customTitle: (
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Target environment - Microsoft Azure
|
||||
</Title>
|
||||
),
|
||||
name: 'ms-azure-target-env',
|
||||
substepOf: 'Target environment',
|
||||
nextStep: ({ values }) =>
|
||||
nextStepMapper(values, {
|
||||
skipAws: true,
|
||||
skipGoogle: true,
|
||||
skipAzure: true,
|
||||
}),
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'azure-description',
|
||||
label: (
|
||||
<TextContent>
|
||||
<Text>
|
||||
Upon build, Image Builder sends the image to the selected authorized
|
||||
Azure account. The image will be uploaded to the resource group in
|
||||
the subscription you specify.
|
||||
</Text>
|
||||
<Text>
|
||||
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" for the resource group you want to upload
|
||||
to. This applies even when defining target by Source selection.
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={AZURE_AUTH_URL}
|
||||
>
|
||||
Learn more about OAuth 2.0
|
||||
</Button>
|
||||
</Text>
|
||||
</TextContent>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.RADIO,
|
||||
label: 'Share method:',
|
||||
name: 'azure-type',
|
||||
initialValue: 'azure-type-source',
|
||||
autoFocus: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Use an account configured from Sources.',
|
||||
description:
|
||||
'Use a configured source to launch environments directly from the console.',
|
||||
value: 'azure-type-source',
|
||||
'data-testid': 'azure-radio-source',
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
label: 'Manually enter the account information.',
|
||||
value: 'azure-type-manual',
|
||||
'data-testid': 'azure-radio-manual',
|
||||
className: 'pf-u-mt-sm',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'azure-sources-select',
|
||||
name: 'azure-sources-select',
|
||||
className: 'pf-u-max-width',
|
||||
label: 'Source Name',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'azure-type',
|
||||
is: 'azure-type-source',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'azure-sources-select-description',
|
||||
label: <SourcesButton />,
|
||||
condition: {
|
||||
when: 'azure-type',
|
||||
is: 'azure-type-source',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gallery-layout',
|
||||
component: 'gallery-layout',
|
||||
minWidths: { default: '12.5rem' },
|
||||
maxWidths: { default: '12.5rem' },
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-tenant-id',
|
||||
'data-testid': 'azure-tenant-id-source',
|
||||
type: 'text',
|
||||
label: 'Azure Tenant GUID',
|
||||
isRequired: true,
|
||||
isReadOnly: true,
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-subscription-id',
|
||||
'data-testid': 'azure-subscription-id-source',
|
||||
type: 'text',
|
||||
label: 'Subscription ID',
|
||||
isRequired: true,
|
||||
isReadOnly: true,
|
||||
condition: {
|
||||
when: 'azure-type',
|
||||
is: 'azure-type-source',
|
||||
},
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'azure-type',
|
||||
is: 'azure-type-source',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-tenant-id',
|
||||
className: 'pf-u-w-50',
|
||||
'data-testid': 'azure-tenant-id-manual',
|
||||
type: 'text',
|
||||
label: 'Azure Tenant GUID',
|
||||
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',
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'azure-type',
|
||||
is: 'azure-type-manual',
|
||||
},
|
||||
},
|
||||
{
|
||||
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-manual',
|
||||
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',
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'azure-type',
|
||||
is: 'azure-type-manual',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'azure-resource-groups',
|
||||
name: 'azure-resource-group',
|
||||
className: 'pf-u-max-width',
|
||||
'data-testid': 'azure-resource-group-select',
|
||||
label: 'Resource group',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'azure-type',
|
||||
is: 'azure-type-source',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-resource-group',
|
||||
className: 'pf-u-w-50',
|
||||
'data-testid': 'azure-resource-group-manual',
|
||||
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',
|
||||
},
|
||||
],
|
||||
condition: {
|
||||
when: 'azure-type',
|
||||
is: 'azure-type-manual',
|
||||
},
|
||||
},
|
||||
// TODO check oauth2 thing too here?
|
||||
],
|
||||
};
|
||||
|
||||
export default msAzureStep;
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
|
||||
import { Text, Title } from '@patternfly/react-core';
|
||||
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import DocumentationButton from '../../sharedComponents/DocumentationButton';
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
const oscapStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-systemconfiguration-oscap',
|
||||
title: 'OpenSCAP',
|
||||
name: 'Compliance',
|
||||
customTitle: (
|
||||
<Title headingLevel="h1" size="xl">
|
||||
OpenSCAP profile
|
||||
</Title>
|
||||
),
|
||||
nextStep: 'File system configuration',
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'oscap-text-component',
|
||||
label: (
|
||||
<Text>
|
||||
Use OpenSCAP to monitor the adherence of your registered RHEL systems
|
||||
to a selected regulatory compliance profile. <DocumentationButton />
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'oscap-profile-selector',
|
||||
name: 'oscap-profile',
|
||||
label: 'Available profiles for the distribution',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default oscapStep;
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
|
||||
import { Text } from '@patternfly/react-core';
|
||||
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
export const reinitPackagesStep = (change) => {
|
||||
change('selected-packages', undefined);
|
||||
};
|
||||
|
||||
const packagesStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-systemconfiguration-packages',
|
||||
title: 'Additional Red Hat packages',
|
||||
name: 'packages',
|
||||
substepOf: 'Content',
|
||||
nextStep: ({ values }) => {
|
||||
if (values.contentSourcesEnabled) {
|
||||
return 'repositories';
|
||||
} else {
|
||||
return 'details';
|
||||
}
|
||||
},
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'packages-text-component',
|
||||
label: (
|
||||
<Text>
|
||||
Images built with Image Builder include all required packages.
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'package-selector',
|
||||
name: 'selected-packages',
|
||||
label: 'Available options',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default packagesStep;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
|
||||
import { Text } from '@patternfly/react-core';
|
||||
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
const packagesContentSourcesStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-systemconfiguration-content-sources-packages',
|
||||
title: 'Additional custom packages',
|
||||
name: 'packages-content-sources',
|
||||
substepOf: 'Content',
|
||||
nextStep: 'details',
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'packages-text-component',
|
||||
label: (
|
||||
<Text>
|
||||
The available packages will return results from all repositories
|
||||
chosen on the previous page.
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'package-selector-content-sources',
|
||||
name: 'selected-packages-content-sources',
|
||||
label: 'Available options',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default packagesContentSourcesStep;
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
import React, { useEffect, useState } 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,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon, HelpIcon } from '@patternfly/react-icons';
|
||||
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import {
|
||||
ACTIVATION_KEYS_PROD_URL,
|
||||
ACTIVATION_KEYS_STAGE_URL,
|
||||
RHC_URL,
|
||||
} from '../../../constants';
|
||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
const ManageKeysButton = () => {
|
||||
const { isProd } = useGetEnvironment();
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={isProd() ? ACTIVATION_KEYS_PROD_URL : ACTIVATION_KEYS_STAGE_URL}
|
||||
>
|
||||
Activation keys page
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const PopoverActivation = () => {
|
||||
const [orgId, setOrgId] = useState(null);
|
||||
const { auth } = useChrome();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const userData = await auth?.getUser();
|
||||
const id = userData?.identity?.internal?.org_id;
|
||||
setOrgId(id);
|
||||
})();
|
||||
});
|
||||
return (
|
||||
<Popover
|
||||
hasAutoWidth
|
||||
maxWidth="35rem"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Activation keys enable you to register a system with appropriate
|
||||
subscriptions, system purpose, and repositories attached.
|
||||
</Text>
|
||||
<Text>
|
||||
If using an activation key with command line registration, you must
|
||||
provide your organization's ID.
|
||||
{orgId && <br />}
|
||||
{orgId && "Your organization's ID is " + orgId}
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="Activation key popover"
|
||||
aria-describedby="subscription-activation-key"
|
||||
className="pf-c-form__group-label-help"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const registrationStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-registration',
|
||||
title: 'Register',
|
||||
customTitle: (
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Register systems using this image
|
||||
</Title>
|
||||
),
|
||||
name: 'registration',
|
||||
nextStep: ({ values }) => {
|
||||
if (values.enableOscap) {
|
||||
return 'Compliance';
|
||||
} else {
|
||||
return 'File system configuration';
|
||||
}
|
||||
},
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'registration-general-description',
|
||||
label:
|
||||
'Automatically register your systems with Red Hat to enhance security and track your spending.',
|
||||
},
|
||||
{
|
||||
name: 'register-system',
|
||||
component: 'registration',
|
||||
label: 'Registration method',
|
||||
initialValue: 'register-now-rhc',
|
||||
},
|
||||
{
|
||||
component: 'activation-keys',
|
||||
name: 'subscription-activation-key',
|
||||
required: true,
|
||||
label: (
|
||||
<>
|
||||
Activation key to use for this image
|
||||
<PopoverActivation />
|
||||
</>
|
||||
),
|
||||
condition: {
|
||||
or: [
|
||||
{ when: 'register-system', is: 'register-now-rhc' },
|
||||
{ when: 'register-system', is: 'register-now-insights' },
|
||||
{ when: 'register-system', is: 'register-now' },
|
||||
],
|
||||
},
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'subscription-activation-description',
|
||||
label: (
|
||||
<span>
|
||||
By default, activation key is generated and preset for you. Admins can
|
||||
create and manage keys by visiting the
|
||||
<ManageKeysButton />
|
||||
</span>
|
||||
),
|
||||
condition: {
|
||||
or: [
|
||||
{ when: 'register-system', is: 'register-now-rhc' },
|
||||
{ when: 'register-system', is: 'register-now-insights' },
|
||||
{ when: 'register-system', is: 'register-now' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'subscription-register-later',
|
||||
label: (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Register Later</Text>
|
||||
<Text>
|
||||
On initial boot, systems will need to be registered manually before
|
||||
having access to updates or Red Hat services. Registering and
|
||||
connecting your systems during the image creation is recommended.
|
||||
</Text>
|
||||
<Text>
|
||||
If you prefer to register later, review the instructions for manual
|
||||
registration with remote host configuration.
|
||||
</Text>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={RHC_URL}
|
||||
>
|
||||
Registering with remote host configuration
|
||||
</Button>
|
||||
</TextContent>
|
||||
),
|
||||
condition: {
|
||||
or: [{ when: 'register-system', is: 'register-later' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'activation-key-information',
|
||||
name: 'subscription-activation-key-information',
|
||||
label: 'Selected activation key',
|
||||
valueReference: 'subscription-activation-key',
|
||||
condition: {
|
||||
or: [
|
||||
{ when: 'register-system', is: 'register-now-rhc' },
|
||||
{ when: 'register-system', is: 'register-now-insights' },
|
||||
{ when: 'register-system', is: 'register-now' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default registrationStep;
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
|
||||
import { Button, Text } from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import nextStepMapper from './repositoriesStepMapper';
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
const VisitButton = () => {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
iconPosition="right"
|
||||
isInline
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
href={isBeta() ? '/preview/settings/content' : '/settings/content'}
|
||||
>
|
||||
Create and manage repositories here
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const repositoriesStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-repositories',
|
||||
title: 'Custom repositories',
|
||||
name: 'repositories',
|
||||
substepOf: 'Content',
|
||||
nextStep: ({ values }) => nextStepMapper(values),
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'packages-text-component',
|
||||
label: (
|
||||
<Text>
|
||||
Select from linked custom repositories from which to search and add
|
||||
packages to this image.
|
||||
<br />
|
||||
<VisitButton />
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'repositories-table',
|
||||
name: 'payload-repositories',
|
||||
label: 'Custom repositories',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default repositoriesStep;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
const repositoriesStepMapper = ({
|
||||
'payload-repositories': customRepositories,
|
||||
} = {}) => {
|
||||
if (customRepositories?.length > 0) {
|
||||
return 'packages-content-sources';
|
||||
}
|
||||
|
||||
return 'details';
|
||||
};
|
||||
|
||||
export default repositoriesStepMapper;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import StepTemplate from './stepTemplate';
|
||||
|
||||
import CustomButtons from '../formComponents/CustomButtons';
|
||||
|
||||
const reviewStep = {
|
||||
StepTemplate,
|
||||
id: 'wizard-review',
|
||||
name: 'review',
|
||||
title: 'Review',
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
name: 'review',
|
||||
component: 'review',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default reviewStep;
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Title } from '@patternfly/react-core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const StepTemplate = ({
|
||||
id,
|
||||
formFields,
|
||||
formRef,
|
||||
title,
|
||||
customTitle,
|
||||
showTitle,
|
||||
showTitles,
|
||||
}) => (
|
||||
<div id={id} ref={formRef} className="pf-c-form">
|
||||
{((showTitles && showTitle !== false) || showTitle) &&
|
||||
(customTitle ? (
|
||||
customTitle
|
||||
) : (
|
||||
<Title headingLevel="h1" size="xl">
|
||||
{title}
|
||||
</Title>
|
||||
))}
|
||||
{formFields}
|
||||
</div>
|
||||
);
|
||||
|
||||
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) }),
|
||||
]),
|
||||
};
|
||||
|
||||
export default StepTemplate;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
const FileSystemConfigurationValidator = () => (fsc) => {
|
||||
if (!fsc) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mpFreqs = {};
|
||||
for (const fs of fsc) {
|
||||
const mp = fs.mountpoint;
|
||||
if (mp in mpFreqs) {
|
||||
mpFreqs[mp]++;
|
||||
} else {
|
||||
mpFreqs[mp] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const duplicates = [];
|
||||
for (const [k, v] of Object.entries(mpFreqs)) {
|
||||
if (v > 1) {
|
||||
duplicates.push(k);
|
||||
}
|
||||
}
|
||||
|
||||
const root = mpFreqs['/'] >= 1;
|
||||
return duplicates.length === 0 && root
|
||||
? undefined
|
||||
: {
|
||||
duplicates: duplicates === [] ? undefined : duplicates,
|
||||
root,
|
||||
};
|
||||
};
|
||||
|
||||
export default FileSystemConfigurationValidator;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default as fileSystemConfigurationValidator } from './fileSystemConfigurationValidator';
|
||||
export { default as targetEnvironmentValidator } from './targetEnvironmentValidator';
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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
|
||||
const valid = Object.values(targets).reduce(
|
||||
(prev, curr) => curr || prev,
|
||||
false
|
||||
);
|
||||
return !valid ? 'Please select an image' : undefined;
|
||||
};
|
||||
|
||||
export default TargetEnvironmentValidator;
|
||||
|
|
@ -191,7 +191,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ImageBuilderHeader />
|
||||
<ImageBuilderHeader inWizard />
|
||||
<section className="pf-l-page__main-section pf-c-page__main-section">
|
||||
<Wizard
|
||||
startIndex={startIndex}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,11 @@ import {
|
|||
PlusCircleIcon,
|
||||
SearchIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
CREATING_IMAGES_WITH_IB_SERVICE_URL,
|
||||
MANAGING_WITH_DNF_URL,
|
||||
} from '../../constants';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
||||
import { BuildImagesButton } from '../Blueprints/BuildImagesButton';
|
||||
|
||||
type ImagesEmptyStateProps = {
|
||||
|
|
@ -57,84 +54,58 @@ const EmptyBlueprintsImagesTable = () => (
|
|||
);
|
||||
|
||||
const EmptyImagesTable = () => {
|
||||
const experimentalFlag = useExperimentalFlag();
|
||||
return (
|
||||
<Bullseye>
|
||||
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
|
||||
{experimentalFlag ? (
|
||||
<>
|
||||
<EmptyStateHeader
|
||||
titleText="No images"
|
||||
icon={<EmptyStateIcon icon={SearchIcon} />}
|
||||
headingLevel="h4"
|
||||
/>
|
||||
<EmptyStateBody>
|
||||
<Text>Images are BLANK. Create blueprints to create images.</Text>
|
||||
</EmptyStateBody>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EmptyStateHeader
|
||||
titleText="Create an RPM-DNF image"
|
||||
icon={<EmptyStateIcon icon={PlusCircleIcon} />}
|
||||
headingLevel="h4"
|
||||
/>
|
||||
<EmptyStateBody>
|
||||
<Text>
|
||||
Image builder is a tool for creating deployment-ready customized
|
||||
system images: installation disks, virtual machines, cloud
|
||||
vendor-specific images, and others. By using image builder, you
|
||||
can create these images faster than with manual procedures
|
||||
because it eliminates the specific configurations required for
|
||||
each output type.
|
||||
</Text>
|
||||
<br />
|
||||
<Text>
|
||||
With RPM-DNF, you can manage the system software by using the
|
||||
DNF package manager and updated RPM packages. This is a simple
|
||||
and adaptive method of managing and modifying the system over
|
||||
its lifecycle.
|
||||
</Text>
|
||||
<br />
|
||||
<Text>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={MANAGING_WITH_DNF_URL}
|
||||
>
|
||||
Learn more about managing images with DNF
|
||||
</Button>
|
||||
</Text>
|
||||
</EmptyStateBody>
|
||||
<EmptyStateFooter>
|
||||
<Link
|
||||
to={resolveRelPath('imagewizard')}
|
||||
className="pf-c-button pf-m-primary"
|
||||
data-testid="create-image-action-empty-state"
|
||||
<>
|
||||
<EmptyStateHeader
|
||||
titleText="No images"
|
||||
icon={<EmptyStateIcon icon={SearchIcon} />}
|
||||
headingLevel="h4"
|
||||
/>
|
||||
<EmptyStateBody>
|
||||
<Text>
|
||||
Image builder is a tool for creating deployment-ready customized
|
||||
system images: installation disks, virtual machines, cloud
|
||||
vendor-specific images, and others. By using image builder, you
|
||||
can create these images faster than with manual procedures because
|
||||
it eliminates the specific configurations required for each output
|
||||
type.
|
||||
</Text>
|
||||
<Text>
|
||||
There are no images yet. Create a blueprint to create images.
|
||||
</Text>
|
||||
<Text>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={MANAGING_WITH_DNF_URL}
|
||||
>
|
||||
Create image
|
||||
</Link>
|
||||
<EmptyStateActions>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={CREATING_IMAGES_WITH_IB_SERVICE_URL}
|
||||
className="pf-u-pt-md"
|
||||
>
|
||||
Image builder for RPM-DNF documentation
|
||||
</Button>
|
||||
</EmptyStateActions>
|
||||
</EmptyStateFooter>
|
||||
</>
|
||||
)}
|
||||
Learn more about managing images with DNF
|
||||
</Button>
|
||||
</Text>
|
||||
</EmptyStateBody>
|
||||
<EmptyStateFooter>
|
||||
<EmptyStateActions>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={CREATING_IMAGES_WITH_IB_SERVICE_URL}
|
||||
className="pf-u-pt-md"
|
||||
>
|
||||
Image builder for RPM-DNF documentation
|
||||
</Button>
|
||||
</EmptyStateActions>
|
||||
</EmptyStateFooter>
|
||||
</>
|
||||
</EmptyState>
|
||||
</Bullseye>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ import {
|
|||
timestampToDisplayString,
|
||||
timestampToDisplayStringDetailed,
|
||||
} from '../../Utilities/time';
|
||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
||||
|
||||
const ImagesTable = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
|
|
@ -93,7 +92,6 @@ const ImagesTable = () => {
|
|||
}),
|
||||
}
|
||||
);
|
||||
const experimentalFlag = useExperimentalFlag();
|
||||
const onSetPage: OnSetPage = (_, page) => setPage(page);
|
||||
|
||||
const onPerPageSelect: OnSetPage = (_, perPage) => {
|
||||
|
|
@ -198,7 +196,7 @@ const ImagesTable = () => {
|
|||
<Th>Updated</Th>
|
||||
<Th>OS</Th>
|
||||
<Th>Target</Th>
|
||||
{experimentalFlag && <Th>Version</Th>}
|
||||
<Th>Version</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Instance</Th>
|
||||
<Th aria-label="Actions menu" />
|
||||
|
|
@ -407,7 +405,6 @@ type AwsRowPropTypes = {
|
|||
|
||||
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
||||
const navigate = useNavigate();
|
||||
const experimentalFlag = useExperimentalFlag();
|
||||
|
||||
const target = <AwsTarget compose={compose} />;
|
||||
|
||||
|
|
@ -418,9 +415,7 @@ const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
|||
const details = <AwsDetails compose={compose} />;
|
||||
|
||||
const actions = (
|
||||
<ActionsColumn
|
||||
items={awsActions(compose, composeStatus, navigate, experimentalFlag)}
|
||||
/>
|
||||
<ActionsColumn items={awsActions(compose, composeStatus, navigate)} />
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -457,8 +452,6 @@ const Row = ({
|
|||
}: RowPropTypes) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const handleToggle = () => setIsExpanded(!isExpanded);
|
||||
const experimentalFlag = useExperimentalFlag();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Tbody key={compose.id} isExpanded={isExpanded}>
|
||||
|
|
@ -483,20 +476,16 @@ const Row = ({
|
|||
<Td dataLabel="Target">
|
||||
{target ? target : <Target compose={compose} />}
|
||||
</Td>
|
||||
{experimentalFlag && (
|
||||
<Td dataLabel="Version">
|
||||
<Badge isRead>{compose.blueprint_version || 'N/A'}</Badge>
|
||||
</Td>
|
||||
)}
|
||||
<Td dataLabel="Version">
|
||||
<Badge isRead>{compose.blueprint_version || 'N/A'}</Badge>
|
||||
</Td>
|
||||
<Td dataLabel="Status">{status}</Td>
|
||||
<Td dataLabel="Instance">{instance}</Td>
|
||||
<Td>
|
||||
{actions ? (
|
||||
actions
|
||||
) : (
|
||||
<ActionsColumn
|
||||
items={defaultActions(compose, navigate, experimentalFlag)}
|
||||
/>
|
||||
<ActionsColumn items={defaultActions(compose)} />
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
|
|
@ -509,21 +498,7 @@ const Row = ({
|
|||
);
|
||||
};
|
||||
|
||||
const defaultActions = (
|
||||
compose: ComposesResponseItem,
|
||||
navigate: NavigateFunction,
|
||||
experimentalFlag: boolean
|
||||
) => [
|
||||
...(experimentalFlag
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: 'Recreate image',
|
||||
onClick: () => {
|
||||
navigate(resolveRelPath(`imagewizard/${compose.id}`));
|
||||
},
|
||||
},
|
||||
]),
|
||||
const defaultActions = (compose: ComposesResponseItem) => [
|
||||
{
|
||||
title: (
|
||||
<a
|
||||
|
|
@ -542,15 +517,14 @@ const defaultActions = (
|
|||
const awsActions = (
|
||||
compose: ComposesResponseItem,
|
||||
status: ComposeStatus | undefined,
|
||||
navigate: NavigateFunction,
|
||||
experimentalFlag: boolean
|
||||
navigate: NavigateFunction
|
||||
) => [
|
||||
{
|
||||
title: 'Share to new region',
|
||||
onClick: () => navigate(resolveRelPath(`share/${compose.id}`)),
|
||||
isDisabled: status?.image_status.status === 'success' ? false : true,
|
||||
},
|
||||
...defaultActions(compose, navigate, experimentalFlag),
|
||||
...defaultActions(compose),
|
||||
];
|
||||
|
||||
export default ImagesTable;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
ToolbarItem,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
selectSelectedBlueprintId,
|
||||
|
|
@ -21,8 +20,6 @@ import {
|
|||
useGetBlueprintsQuery,
|
||||
useGetBlueprintComposesQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
||||
import { BlueprintActionsMenu } from '../Blueprints/BlueprintActionsMenu';
|
||||
import BlueprintVersionFilter from '../Blueprints/BlueprintVersionFilter';
|
||||
import { BuildImagesButton } from '../Blueprints/BuildImagesButton';
|
||||
|
|
@ -44,7 +41,6 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
|||
setPage,
|
||||
onPerPageSelect,
|
||||
}: imagesTableToolbarProps) => {
|
||||
const experimentalFlag = useExperimentalFlag();
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
||||
|
|
@ -98,27 +94,6 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
|||
/>
|
||||
);
|
||||
|
||||
if (!experimentalFlag) {
|
||||
return (
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<Link
|
||||
to={resolveRelPath('imagewizard')}
|
||||
className="pf-c-button pf-m-primary"
|
||||
data-testid="create-image-action"
|
||||
>
|
||||
Create image
|
||||
</Link>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem variant="pagination" align={{ default: 'alignRight' }}>
|
||||
{pagination}
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
|
||||
const isBlueprintDistroCentos8 = () => {
|
||||
if (isSuccessBlueprintsCompose) {
|
||||
return blueprintsComposes.data[0].request.distribution === 'centos-8';
|
||||
|
|
@ -139,7 +114,7 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
|||
: 'All images'}
|
||||
</Title>
|
||||
</ToolbarContent>
|
||||
{itemCount > 0 && experimentalFlag && isBlueprintOutSync && (
|
||||
{itemCount > 0 && isBlueprintOutSync && (
|
||||
<Alert
|
||||
style={{
|
||||
margin:
|
||||
|
|
@ -164,25 +139,28 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
|||
ouiaId="centos-8-blueprint-alert"
|
||||
/>
|
||||
)}
|
||||
{selectedBlueprintId && (
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<BlueprintVersionFilter onFilterChange={() => setPage(1)} />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<BuildImagesButton />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<EditBlueprintButton />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<BlueprintActionsMenu setShowDeleteModal={setShowDeleteModal} />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem variant="pagination" align={{ default: 'alignRight' }}>
|
||||
{pagination}
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
)}
|
||||
|
||||
<ToolbarContent>
|
||||
{selectedBlueprintId && (
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<BlueprintVersionFilter onFilterChange={() => setPage(1)} />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<BuildImagesButton />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<EditBlueprintButton />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<BlueprintActionsMenu setShowDeleteModal={setShowDeleteModal} />
|
||||
</ToolbarItem>
|
||||
</>
|
||||
)}
|
||||
<ToolbarItem variant="pagination" align={{ default: 'alignRight' }}>
|
||||
{pagination}
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ import {
|
|||
isOciUploadStatus,
|
||||
} from '../../store/typeGuards';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
||||
import useProvisioningPermissions from '../../Utilities/useProvisioningPermissions';
|
||||
|
||||
type CloudInstancePropTypes = {
|
||||
|
|
@ -347,9 +346,6 @@ export const AwsS3Instance = ({
|
|||
composeId: compose.id,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const experimentalFlag = useExperimentalFlag();
|
||||
|
||||
if (!isSuccess) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
|
@ -388,18 +384,6 @@ export const AwsS3Instance = ({
|
|||
)
|
||||
</Button>
|
||||
);
|
||||
} else if (isExpired && !experimentalFlag) {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
onClick={() => navigate(resolveRelPath(`imagewizard/${compose.id}`))}
|
||||
isInline
|
||||
>
|
||||
Recreate image
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -24,12 +24,10 @@ import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|||
import './LandingPage.scss';
|
||||
|
||||
import { NewAlert } from './NewAlert';
|
||||
import Quickstarts from './Quickstarts';
|
||||
|
||||
import { MANAGING_WITH_DNF_URL, OSTREE_URL } from '../../constants';
|
||||
import { manageEdgeImagesUrlName } from '../../Utilities/edge';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
||||
import BlueprintsSidebar from '../Blueprints/BlueprintsSideBar';
|
||||
import EdgeImagesTable from '../edge/ImagesTable';
|
||||
import ImagesTable from '../ImagesTable/ImagesTable';
|
||||
|
|
@ -58,20 +56,8 @@ export const LandingPage = () => {
|
|||
};
|
||||
|
||||
const edgeParityFlag = useFlag('edgeParity.image-list');
|
||||
const experimentalFlag = useExperimentalFlag();
|
||||
|
||||
const traditionalImageList = (
|
||||
<>
|
||||
<PageSection>
|
||||
<Quickstarts />
|
||||
</PageSection>
|
||||
<PageSection>
|
||||
<ImagesTable />
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
||||
const experimentalImageList = (
|
||||
const imageList = (
|
||||
<>
|
||||
<PageSection isWidthLimited>
|
||||
{showAlert && <NewAlert setShowAlert={setShowAlert} />}
|
||||
|
|
@ -98,16 +84,9 @@ export const LandingPage = () => {
|
|||
</>
|
||||
);
|
||||
|
||||
const imageList = experimentalFlag
|
||||
? experimentalImageList
|
||||
: traditionalImageList;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ImageBuilderHeader
|
||||
experimentalFlag={experimentalFlag}
|
||||
activeTab={activeTabKey}
|
||||
/>
|
||||
<ImageBuilderHeader activeTab={activeTabKey} />
|
||||
{edgeParityFlag ? (
|
||||
<Tabs
|
||||
className="pf-c-tabs pf-c-page-header pf-c-table"
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ import './ImageBuilderHeader.scss';
|
|||
import { ImportBlueprintModal } from '../Blueprints/ImportBlueprintModal';
|
||||
|
||||
type ImageBuilderHeaderPropTypes = {
|
||||
experimentalFlag?: boolean;
|
||||
activeTab?: number;
|
||||
inWizard?: boolean;
|
||||
};
|
||||
|
||||
const AboutImageBuilderPopover = () => {
|
||||
|
|
@ -92,8 +92,8 @@ const AboutImageBuilderPopover = () => {
|
|||
};
|
||||
|
||||
export const ImageBuilderHeader = ({
|
||||
experimentalFlag,
|
||||
activeTab,
|
||||
inWizard,
|
||||
}: ImageBuilderHeaderPropTypes) => {
|
||||
const navigate = useNavigate();
|
||||
const importExportFlag = useFlag('image-builder.import.enabled');
|
||||
|
|
@ -122,7 +122,7 @@ export const ImageBuilderHeader = ({
|
|||
}
|
||||
/>
|
||||
</FlexItem>
|
||||
{experimentalFlag && (
|
||||
{!inWizard && (
|
||||
<>
|
||||
<FlexItem align={{ default: 'alignRight' }}>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -6,12 +6,8 @@ import { Route, Routes } from 'react-router-dom';
|
|||
import EdgeImageDetail from './Components/edge/ImageDetails';
|
||||
import ShareImageModal from './Components/ShareImageModal/ShareImageModal';
|
||||
import { manageEdgeImagesUrlName } from './Utilities/edge';
|
||||
import { useExperimentalFlag } from './Utilities/useExperimentalFlag';
|
||||
|
||||
const LandingPage = lazy(() => import('./Components/LandingPage/LandingPage'));
|
||||
const CreateImageWizard = lazy(
|
||||
() => import('./Components/CreateImageWizard/CreateImageWizard')
|
||||
);
|
||||
const ImportImageWizard = lazy(
|
||||
() => import('./Components/CreateImageWizardV2/ImportImageWizard')
|
||||
);
|
||||
|
|
@ -22,7 +18,6 @@ const CreateImageWizardV2 = lazy(
|
|||
export const Router = () => {
|
||||
const edgeParityFlag = useFlag('edgeParity.image-list');
|
||||
const importExportFlag = useFlag('image-builder.import.enabled');
|
||||
const experimentalFlag = useExperimentalFlag();
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
|
|
@ -36,7 +31,7 @@ export const Router = () => {
|
|||
<Route path="share/:composeId" element={<ShareImageModal />} />
|
||||
</Route>
|
||||
|
||||
{importExportFlag && experimentalFlag && (
|
||||
{importExportFlag && (
|
||||
<Route
|
||||
path="imagewizard/import"
|
||||
element={
|
||||
|
|
@ -50,7 +45,7 @@ export const Router = () => {
|
|||
path="imagewizard/:composeId?"
|
||||
element={
|
||||
<Suspense>
|
||||
{experimentalFlag ? <CreateImageWizardV2 /> : <CreateImageWizard />}
|
||||
<CreateImageWizardV2 />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import { useFlag } from '@unleash/proxy-client-react';
|
||||
|
||||
import { useGetEnvironment } from './useGetEnvironment';
|
||||
|
||||
/**
|
||||
* @example
|
||||
* // returns true
|
||||
* toBoolean('true');
|
||||
* @example
|
||||
* // returns false
|
||||
* toBoolean('FALSE');
|
||||
* @example
|
||||
* // returns false
|
||||
* toBoolean(undefined);
|
||||
*/
|
||||
const toBoolean = (environmentVariable: string | undefined): boolean => {
|
||||
return environmentVariable?.toLowerCase() === 'true';
|
||||
};
|
||||
|
||||
export const useExperimentalFlag = () => {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
const isExperimental = toBoolean(process.env.EXPERIMENTAL?.toString());
|
||||
return (
|
||||
useFlag('image-builder.new-wizard.stable') || isBeta() || isExperimental
|
||||
);
|
||||
};
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
|
||||
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
|
||||
import { PROVISIONING_API } from '../../../constants';
|
||||
import { server } from '../../mocks/server';
|
||||
import {
|
||||
clickBack,
|
||||
clickNext,
|
||||
getNextButton,
|
||||
renderCustomRoutesWithReduxRouter,
|
||||
verifyCancelButton,
|
||||
} from '../../testUtils';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: 'insights/image-builder/*',
|
||||
element: <div />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/imagewizard/:composeId?',
|
||||
element: <CreateImageWizard />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/share/:composeId',
|
||||
element: <ShareImageModal />,
|
||||
},
|
||||
];
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
useChrome: () => ({
|
||||
auth: {
|
||||
getUser: () => {
|
||||
return {
|
||||
identity: {
|
||||
internal: {
|
||||
org_id: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
isBeta: () => false,
|
||||
isProd: () => true,
|
||||
getEnvironment: () => 'prod',
|
||||
}),
|
||||
}));
|
||||
|
||||
let router = undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
// scrollTo is not defined in jsdom
|
||||
window.HTMLElement.prototype.scrollTo = function () {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
router = undefined;
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
const getSourceDropdown = async () => {
|
||||
const sourceDropdown = await screen.findByRole('textbox', {
|
||||
name: /select source/i,
|
||||
});
|
||||
// Wait for isSuccess === true, dropdown is disabled while isSuccess === false
|
||||
await waitFor(() => expect(sourceDropdown).toBeEnabled());
|
||||
return sourceDropdown;
|
||||
};
|
||||
|
||||
describe('Step Upload to Azure', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
));
|
||||
// select Azure as upload destination
|
||||
await user.click(await screen.findByTestId('upload-azure'));
|
||||
|
||||
await clickNext();
|
||||
|
||||
await screen.findByRole('heading', {
|
||||
name: 'Target environment - Microsoft Azure',
|
||||
});
|
||||
};
|
||||
|
||||
test('clicking Next loads Registration', async () => {
|
||||
await setUp();
|
||||
|
||||
await user.click(await screen.findByTestId('azure-radio-manual'));
|
||||
// Randomly generated GUID
|
||||
await user.type(
|
||||
await screen.findByTestId('azure-tenant-id-manual'),
|
||||
'b8f86d22-4371-46ce-95e7-65c415f3b1e2'
|
||||
);
|
||||
await user.type(
|
||||
await screen.findByTestId('azure-subscription-id-manual'),
|
||||
'60631143-a7dc-4d15-988b-ba83f3c99711'
|
||||
);
|
||||
await user.type(
|
||||
await screen.findByTestId('azure-resource-group-manual'),
|
||||
'testResourceGroup'
|
||||
);
|
||||
await clickNext();
|
||||
|
||||
await screen.findByRole('textbox', {
|
||||
name: 'Select activation key',
|
||||
});
|
||||
|
||||
await screen.findByText(
|
||||
'Automatically register and enable advanced capabilities'
|
||||
);
|
||||
});
|
||||
|
||||
test('clicking Back loads Release', async () => {
|
||||
await setUp();
|
||||
|
||||
await clickBack();
|
||||
|
||||
await screen.findByTestId('upload-azure');
|
||||
});
|
||||
|
||||
test('clicking Cancel loads landing page', async () => {
|
||||
await setUp();
|
||||
|
||||
await verifyCancelButton(router);
|
||||
});
|
||||
|
||||
test('azure step basics works', async () => {
|
||||
await setUp();
|
||||
const nextButton = await getNextButton();
|
||||
|
||||
expect(nextButton).toHaveClass('pf-m-disabled');
|
||||
expect(await screen.findByTestId('azure-radio-source')).toBeChecked();
|
||||
|
||||
await user.click(await screen.findByTestId('azure-radio-manual'));
|
||||
expect(await screen.findByTestId('azure-radio-manual')).toBeChecked();
|
||||
|
||||
expect(nextButton).toHaveClass('pf-m-disabled');
|
||||
|
||||
const tenantId = await screen.findByTestId('azure-tenant-id-manual');
|
||||
expect(tenantId).toHaveValue('');
|
||||
expect(tenantId).toBeEnabled();
|
||||
await user.type(tenantId, 'c983c2cd-94d7-44e1-9c6e-9cfa3a40995f');
|
||||
const subscription = await screen.findByTestId(
|
||||
'azure-subscription-id-manual'
|
||||
);
|
||||
expect(subscription).toHaveValue('');
|
||||
expect(subscription).toBeEnabled();
|
||||
await user.type(subscription, 'f8f200aa-6234-4bfb-86c2-163d33dffc0c');
|
||||
const resourceGroup = await screen.findByTestId(
|
||||
'azure-resource-group-manual'
|
||||
);
|
||||
expect(resourceGroup).toHaveValue('');
|
||||
expect(resourceGroup).toBeEnabled();
|
||||
await user.type(resourceGroup, 'testGroup');
|
||||
|
||||
expect(nextButton).not.toHaveClass('pf-m-disabled');
|
||||
|
||||
await user.click(await screen.findByTestId('azure-radio-source'));
|
||||
|
||||
await waitFor(() => expect(nextButton).toHaveClass('pf-m-disabled'));
|
||||
|
||||
const sourceDropdown = await getSourceDropdown();
|
||||
|
||||
// manual values should be cleared out
|
||||
expect(await screen.findByTestId('azure-tenant-id-source')).toHaveValue('');
|
||||
expect(
|
||||
await screen.findByTestId('azure-subscription-id-source')
|
||||
).toHaveValue('');
|
||||
|
||||
await user.click(sourceDropdown);
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('option', {
|
||||
name: /azureSource1/i,
|
||||
})
|
||||
);
|
||||
// wait for fetching the upload info
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('azure-tenant-id-source')).not.toHaveValue('')
|
||||
);
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('textbox', {
|
||||
name: /select resource group/i,
|
||||
})
|
||||
);
|
||||
const groups = screen.getAllByLabelText(/^Resource group/);
|
||||
expect(groups).toHaveLength(2);
|
||||
await user.click(
|
||||
await screen.findByLabelText('Resource group myResourceGroup1')
|
||||
);
|
||||
|
||||
expect(nextButton).not.toHaveClass('pf-m-disabled');
|
||||
}, 10000);
|
||||
|
||||
test('handles change of selected Source', async () => {
|
||||
await setUp();
|
||||
|
||||
const sourceDropdown = await getSourceDropdown();
|
||||
|
||||
await user.click(sourceDropdown);
|
||||
await user.click(
|
||||
await screen.findByRole('option', {
|
||||
name: /azureSource1/i,
|
||||
})
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('azure-tenant-id-source')).not.toHaveValue('')
|
||||
);
|
||||
|
||||
await user.click(sourceDropdown);
|
||||
await user.click(
|
||||
await screen.findByRole('option', {
|
||||
name: /azureSource2/i,
|
||||
})
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('azure-tenant-id-source')).toHaveValue(
|
||||
'73d5694c-7a28-417e-9fca-55840084f508'
|
||||
);
|
||||
});
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('textbox', {
|
||||
name: /select resource group/i,
|
||||
})
|
||||
);
|
||||
const groups = await screen.findByLabelText(/^Resource group/);
|
||||
expect(groups).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByLabelText('Resource group theirGroup2')
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('component renders error state correctly', async () => {
|
||||
server.use(
|
||||
rest.get(`${PROVISIONING_API}/sources`, (req, res, ctx) =>
|
||||
res(ctx.status(500))
|
||||
)
|
||||
);
|
||||
|
||||
await setUp();
|
||||
|
||||
await screen.findByText(
|
||||
/Sources cannot be reached, try again later or enter an account info for upload manually\./i
|
||||
);
|
||||
});
|
||||
|
||||
// set test timeout to 15 seconds
|
||||
}, 15000);
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
|
||||
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
|
||||
import {
|
||||
clickNext,
|
||||
renderCustomRoutesWithReduxRouter,
|
||||
renderWithReduxRouter,
|
||||
} from '../../testUtils';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: 'insights/image-builder/*',
|
||||
element: <div />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/imagewizard/:composeId?',
|
||||
element: <CreateImageWizard />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/share/:composeId',
|
||||
element: <ShareImageModal />,
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
useChrome: () => ({
|
||||
auth: {
|
||||
getUser: () => {
|
||||
return {
|
||||
identity: {
|
||||
internal: {
|
||||
org_id: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
isBeta: () => true,
|
||||
isProd: () => false,
|
||||
getEnvironment: () => 'stage',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@unleash/proxy-client-react', () => ({
|
||||
useUnleashContext: () => jest.fn(),
|
||||
useFlag: jest.fn((flag) =>
|
||||
flag === 'image-builder.wizard.oscap.enabled' ? true : false
|
||||
),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
// scrollTo is not defined in jsdom
|
||||
window.HTMLElement.prototype.scrollTo = function () {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Step Compliance', () => {
|
||||
const user = userEvent.setup();
|
||||
const setup = async () => {
|
||||
renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
|
||||
};
|
||||
test('create an image with None oscap profile', async () => {
|
||||
await setup();
|
||||
|
||||
// select aws as upload destination
|
||||
await user.click(await screen.findByTestId('upload-aws'));
|
||||
await clickNext();
|
||||
|
||||
// aws step
|
||||
await user.click(
|
||||
await screen.findByRole('radio', {
|
||||
name: /manually enter an account id\./i,
|
||||
})
|
||||
);
|
||||
await user.type(
|
||||
await screen.findByTestId('aws-account-id'),
|
||||
'012345678901'
|
||||
);
|
||||
|
||||
await clickNext();
|
||||
// skip registration
|
||||
await user.click(await screen.findByLabelText('Register later'));
|
||||
await clickNext();
|
||||
|
||||
// Now we should be in the Compliance step
|
||||
await screen.findByRole('heading', { name: /OpenSCAP/i });
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('textbox', { name: /select a profile/i })
|
||||
);
|
||||
await user.click(await screen.findByText(/none/i));
|
||||
|
||||
// check that the FSC does not contain a /tmp partition
|
||||
await clickNext();
|
||||
await screen.findByRole('heading', { name: /File system configuration/i });
|
||||
expect(
|
||||
screen.queryByRole('cell', {
|
||||
name: /tmp/i,
|
||||
})
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// check that there are no Packages contained when selecting the "None" profile option
|
||||
await clickNext();
|
||||
await screen.findByRole('heading', {
|
||||
name: /Additional Red Hat packages/i,
|
||||
});
|
||||
await screen.findByText(/no packages added/i);
|
||||
});
|
||||
|
||||
test('create an image with an oscap profile', async () => {
|
||||
await setup();
|
||||
|
||||
// select aws as upload destination
|
||||
await user.click(await screen.findByTestId('upload-aws'));
|
||||
await clickNext();
|
||||
|
||||
// aws step
|
||||
await user.click(
|
||||
await screen.findByRole('radio', {
|
||||
name: /manually enter an account id\./i,
|
||||
})
|
||||
);
|
||||
await user.type(
|
||||
await screen.findByTestId('aws-account-id'),
|
||||
'012345678901'
|
||||
);
|
||||
|
||||
await clickNext();
|
||||
// skip registration
|
||||
await user.click(await screen.findByLabelText('Register later'));
|
||||
await clickNext();
|
||||
|
||||
// Now we should be at the OpenSCAP step
|
||||
await screen.findByRole('heading', { name: /OpenSCAP/i });
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('textbox', {
|
||||
name: /select a profile/i,
|
||||
})
|
||||
);
|
||||
|
||||
await user.click(
|
||||
await screen.findByText(
|
||||
/cis red hat enterprise linux 8 benchmark for level 1 - workstation/i
|
||||
)
|
||||
);
|
||||
await screen.findByText(/kernel arguments:/i);
|
||||
await screen.findByText(/audit_backlog_limit=8192 audit=1/i);
|
||||
await screen.findByText(/disabled services:/i);
|
||||
await screen.findByText(/nfs-server/i);
|
||||
await screen.findByText(/enabled services:/i);
|
||||
await screen.findByText(/crond/i);
|
||||
|
||||
// check that the FSC contains a /tmp partition
|
||||
await clickNext();
|
||||
await screen.findByRole('heading', { name: /File system configuration/i });
|
||||
await screen.findByText(/tmp/i);
|
||||
|
||||
// check that the Packages contains correct packages
|
||||
await clickNext();
|
||||
|
||||
await screen.findByRole('heading', {
|
||||
name: /Additional Red Hat packages/i,
|
||||
});
|
||||
await screen.findByText(/aide/i);
|
||||
await screen.findByText(/neovim/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('On Recreate', () => {
|
||||
const setup = async () => {
|
||||
renderWithReduxRouter('imagewizard/1679d95b-8f1d-4982-8c53-8c2afa4ab04c');
|
||||
};
|
||||
test('with oscap profile', async () => {
|
||||
const user = userEvent.setup();
|
||||
await setup();
|
||||
await screen.findByRole('button', {
|
||||
name: /review/i,
|
||||
});
|
||||
const createImageButton = await screen.findByRole('button', {
|
||||
name: /create image/i,
|
||||
});
|
||||
await waitFor(() => expect(createImageButton).toBeEnabled());
|
||||
|
||||
// check that the FSC contains a /tmp partition
|
||||
// There are two buttons with the same name but cannot easily select the DDF rendered sidenav.
|
||||
// The sidenav will be the first node found out of all buttons.
|
||||
const buttonsFSC = await screen.findAllByRole('button', {
|
||||
name: /file system configuration/i,
|
||||
});
|
||||
await user.click(buttonsFSC[0]);
|
||||
await screen.findByRole('heading', { name: /file system configuration/i });
|
||||
await screen.findByText('/tmp');
|
||||
|
||||
// check that the Packages contain a nftable package
|
||||
await clickNext();
|
||||
await screen.findByRole('heading', {
|
||||
name: /Additional Red Hat packages/i,
|
||||
});
|
||||
await screen.findByText('nftables');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,676 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import api from '../../../api.js';
|
||||
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
|
||||
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
|
||||
import { mockPkgResultAlphaContentSources } from '../../fixtures/packages';
|
||||
import {
|
||||
clickBack,
|
||||
clickNext,
|
||||
renderCustomRoutesWithReduxRouter,
|
||||
renderWithReduxRouter,
|
||||
} from '../../testUtils';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: 'insights/image-builder/*',
|
||||
element: <div />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/imagewizard/:composeId?',
|
||||
element: <CreateImageWizard />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/share/:composeId',
|
||||
element: <ShareImageModal />,
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
useChrome: () => ({
|
||||
auth: {
|
||||
getUser: () => {
|
||||
return {
|
||||
identity: {
|
||||
internal: {
|
||||
org_id: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
isBeta: () => false,
|
||||
isProd: () => true,
|
||||
getEnvironment: () => 'prod',
|
||||
}),
|
||||
}));
|
||||
|
||||
const searchForAvailablePackages = async (searchbox, searchTerm) => {
|
||||
const user = userEvent.setup();
|
||||
await user.type(searchbox, searchTerm);
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: /search button for available packages/i,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const searchForChosenPackages = async (searchbox, searchTerm) => {
|
||||
const user = userEvent.setup();
|
||||
if (!searchTerm) {
|
||||
await user.clear(searchbox);
|
||||
} else {
|
||||
await user.type(searchbox, searchTerm);
|
||||
}
|
||||
};
|
||||
|
||||
let mockContentSourcesEnabled;
|
||||
jest.mock('@unleash/proxy-client-react', () => ({
|
||||
useUnleashContext: () => jest.fn(),
|
||||
useFlag: jest.fn((flag) =>
|
||||
flag === 'image-builder.enable-content-sources'
|
||||
? mockContentSourcesEnabled
|
||||
: false
|
||||
),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
// scrollTo is not defined in jsdom
|
||||
window.HTMLElement.prototype.scrollTo = function () {};
|
||||
mockContentSourcesEnabled = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockContentSourcesEnabled = true;
|
||||
});
|
||||
|
||||
describe('Step Packages', () => {
|
||||
describe('with Content Sources', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
|
||||
|
||||
// select aws as upload destination
|
||||
await waitFor(
|
||||
async () => await user.click(await screen.findByTestId('upload-aws'))
|
||||
);
|
||||
await clickNext();
|
||||
|
||||
// aws step
|
||||
await user.click(
|
||||
await screen.findByRole('radio', {
|
||||
name: /manually enter an account id\./i,
|
||||
})
|
||||
);
|
||||
await user.type(
|
||||
await screen.findByTestId('aws-account-id'),
|
||||
'012345678901'
|
||||
);
|
||||
await clickNext();
|
||||
// skip registration
|
||||
await screen.findByRole('textbox', {
|
||||
name: 'Select activation key',
|
||||
});
|
||||
|
||||
const registerLaterRadio = await screen.findByTestId(
|
||||
'registration-radio-later'
|
||||
);
|
||||
await user.click(registerLaterRadio);
|
||||
await clickNext();
|
||||
// skip fsc
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
test('search results should be sorted with most relevant results first', async () => {
|
||||
await setUp();
|
||||
|
||||
const view = await screen.findByTestId('search-available-pkgs-input');
|
||||
|
||||
const searchbox = await within(view).findByRole('textbox', {
|
||||
name: /search input/i,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(searchbox).toBeEnabled());
|
||||
await user.click(searchbox);
|
||||
|
||||
await searchForAvailablePackages(searchbox, 'test');
|
||||
|
||||
const availablePackagesList = await screen.findByTestId(
|
||||
'available-pkgs-list'
|
||||
);
|
||||
const availablePackagesItems = await within(
|
||||
availablePackagesList
|
||||
).findAllByRole('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
|
||||
|
||||
await waitFor(() => expect(searchbox).toBeEnabled());
|
||||
await user.click(searchbox);
|
||||
|
||||
await searchForAvailablePackages(searchbox, 'test');
|
||||
|
||||
await user.click(await screen.findByTestId('available-pkgs-testPkg'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: /Add selected/ })
|
||||
);
|
||||
|
||||
await user.click(await screen.findByTestId('selected-pkgs-testPkg'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: /Remove selected/ })
|
||||
);
|
||||
|
||||
const availablePackagesList = await screen.findByTestId(
|
||||
'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
|
||||
|
||||
await waitFor(() => expect(searchbox).toBeEnabled());
|
||||
await user.click(searchbox);
|
||||
|
||||
await searchForAvailablePackages(searchbox, 'test');
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /Add all/ }));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: /Remove all/ })
|
||||
);
|
||||
|
||||
const availablePackagesList = await screen.findByTestId(
|
||||
'available-pkgs-list'
|
||||
);
|
||||
const availablePackagesItems = await within(
|
||||
availablePackagesList
|
||||
).findAllByRole('option');
|
||||
expect(availablePackagesItems).toHaveLength(3);
|
||||
const [firstItem, secondItem, thirdItem] = availablePackagesItems;
|
||||
expect(firstItem).toHaveTextContent('testsummary for test package');
|
||||
// TODO
|
||||
expect(thirdItem).toHaveTextContent('lib-testlib-test package summary');
|
||||
expect(secondItem).toHaveTextContent('testPkgtest 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
|
||||
|
||||
await waitFor(() => expect(searchbox).toBeEnabled());
|
||||
await user.click(searchbox);
|
||||
|
||||
await searchForAvailablePackages(searchbox, 'test');
|
||||
await user.click(await screen.findByRole('button', { name: /Add all/ }));
|
||||
|
||||
// remove a single package
|
||||
await user.click(await screen.findByTestId('selected-pkgs-lib-test'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: /Remove selected/ })
|
||||
);
|
||||
// skip Custom repositories page
|
||||
clickNext();
|
||||
|
||||
// skip name page
|
||||
clickNext();
|
||||
|
||||
// review page
|
||||
clickNext();
|
||||
|
||||
const chosen = await screen.findByTestId('chosen-packages-count');
|
||||
expect(chosen).toHaveTextContent('2');
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
await waitFor(() => expect(searchbox).toBeEnabled());
|
||||
await user.click(searchbox);
|
||||
|
||||
await searchForAvailablePackages(searchbox, 'asdf');
|
||||
|
||||
await screen.findByText('No results 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];
|
||||
|
||||
await waitFor(() => expect(searchboxAvailable).toBeEnabled());
|
||||
await user.click(searchboxAvailable);
|
||||
await searchForAvailablePackages(searchboxAvailable, 'test');
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /Add all/ }));
|
||||
|
||||
await user.click(searchboxChosen);
|
||||
await user.type(searchboxChosen, 'asdf');
|
||||
|
||||
expect(await screen.findByText('No packages found')).toBeInTheDocument();
|
||||
// We need to clear this input in order to not have sideeffects on other tests
|
||||
await searchForChosenPackages(searchboxChosen, '');
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
await waitFor(() => expect(searchbox).toBeEnabled());
|
||||
await user.click(searchbox);
|
||||
|
||||
const getPackages = jest
|
||||
.spyOn(api, 'getPackagesContentSources')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve(mockPkgResultAlphaContentSources)
|
||||
);
|
||||
|
||||
await searchForAvailablePackages(searchbox, 'test');
|
||||
await waitFor(() => expect(getPackages).toHaveBeenCalledTimes(1));
|
||||
|
||||
const availablePackagesList = await screen.findByTestId(
|
||||
'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];
|
||||
|
||||
await waitFor(() => expect(searchbox).toBeEnabled());
|
||||
await user.click(searchbox);
|
||||
|
||||
await searchForAvailablePackages(searchbox, 'test');
|
||||
|
||||
const availablePackagesList = await screen.findByTestId(
|
||||
'available-pkgs-list'
|
||||
);
|
||||
const availablePackagesItems = await within(
|
||||
availablePackagesList
|
||||
).findAllByRole('option');
|
||||
expect(availablePackagesItems).toHaveLength(3);
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: /clear available packages search/i,
|
||||
})
|
||||
);
|
||||
|
||||
await screen.findByText(
|
||||
'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];
|
||||
|
||||
await waitFor(() => expect(availableSearchbox).toBeEnabled());
|
||||
await user.click(availableSearchbox);
|
||||
|
||||
await searchForAvailablePackages(availableSearchbox, 'test');
|
||||
|
||||
const availablePackagesList = await screen.findByTestId(
|
||||
'available-pkgs-list'
|
||||
);
|
||||
const availablePackagesItems = await within(
|
||||
availablePackagesList
|
||||
).findAllByRole('option');
|
||||
expect(availablePackagesItems).toHaveLength(3);
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /Add all/ }));
|
||||
|
||||
const chosenPackagesList = await screen.findByTestId('chosen-pkgs-list');
|
||||
let chosenPackagesItems = await within(chosenPackagesList).findAllByRole(
|
||||
'option'
|
||||
);
|
||||
expect(chosenPackagesItems).toHaveLength(3);
|
||||
|
||||
const chosenSearchbox = screen.getAllByRole('textbox')[1];
|
||||
await user.click(chosenSearchbox);
|
||||
await searchForChosenPackages(chosenSearchbox, 'lib');
|
||||
chosenPackagesItems = within(chosenPackagesList).getAllByRole('option');
|
||||
expect(chosenPackagesItems).toHaveLength(1);
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: /clear chosen packages search/i,
|
||||
})
|
||||
);
|
||||
chosenPackagesItems = await within(chosenPackagesList).findAllByRole(
|
||||
'option'
|
||||
);
|
||||
await waitFor(() => expect(chosenPackagesItems).toHaveLength(3));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Step Custom repositories', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
|
||||
|
||||
// select aws as upload destination
|
||||
await user.click(await screen.findByTestId('upload-aws'));
|
||||
await clickNext();
|
||||
|
||||
// aws step
|
||||
await user.click(
|
||||
await screen.findByRole('radio', {
|
||||
name: /manually enter an account id\./i,
|
||||
})
|
||||
);
|
||||
await user.type(
|
||||
await screen.findByTestId('aws-account-id'),
|
||||
'012345678901'
|
||||
);
|
||||
|
||||
await clickNext();
|
||||
// skip registration
|
||||
await screen.findByRole('textbox', {
|
||||
name: 'Select activation key',
|
||||
});
|
||||
|
||||
await user.click(await screen.findByLabelText('Register later'));
|
||||
await clickNext();
|
||||
// skip fsc
|
||||
await clickNext();
|
||||
// skip packages
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
test('selected repositories stored in and retrieved from form state', async () => {
|
||||
await setUp();
|
||||
|
||||
const getFirstRepoCheckbox = async () =>
|
||||
await screen.findByRole('checkbox', {
|
||||
name: /select row 0/i,
|
||||
});
|
||||
let firstRepoCheckbox = await getFirstRepoCheckbox();
|
||||
|
||||
expect(firstRepoCheckbox.checked).toEqual(false);
|
||||
await user.click(firstRepoCheckbox);
|
||||
await waitFor(() => expect(firstRepoCheckbox.checked).toEqual(true));
|
||||
|
||||
await clickNext();
|
||||
await clickBack();
|
||||
|
||||
firstRepoCheckbox = await getFirstRepoCheckbox();
|
||||
await waitFor(() => expect(firstRepoCheckbox.checked).toEqual(true));
|
||||
});
|
||||
|
||||
test('correct number of repositories is fetched', async () => {
|
||||
await setUp();
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: /^select$/i,
|
||||
})
|
||||
);
|
||||
|
||||
await screen.findByText(/select all \(1016 items\)/i);
|
||||
});
|
||||
|
||||
test('filter works', async () => {
|
||||
await setUp();
|
||||
|
||||
await user.type(
|
||||
await screen.findByRole('textbox', { name: /search repositories/i }),
|
||||
'2zmya'
|
||||
);
|
||||
|
||||
const table = await screen.findByTestId('repositories-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const getRows = () => getAllByRole('row');
|
||||
|
||||
let rows = getRows();
|
||||
// remove first row from list since it is just header labels
|
||||
rows.shift();
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
|
||||
// clear filter
|
||||
await user.click(await screen.findByRole('button', { name: /reset/i }));
|
||||
|
||||
rows = getRows();
|
||||
// remove first row from list since it is just header labels
|
||||
rows.shift();
|
||||
|
||||
await waitFor(() => expect(rows).toHaveLength(10));
|
||||
});
|
||||
|
||||
test('press on Selected button to see selected repositories list', async () => {
|
||||
await setUp();
|
||||
|
||||
const getFirstRepoCheckbox = async () =>
|
||||
await screen.findByRole('checkbox', {
|
||||
name: /select row 0/i,
|
||||
});
|
||||
const firstRepoCheckbox = await getFirstRepoCheckbox();
|
||||
|
||||
expect(firstRepoCheckbox.checked).toEqual(false);
|
||||
await user.click(firstRepoCheckbox);
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
|
||||
const getSelectedButton = async () =>
|
||||
await screen.findByRole('button', {
|
||||
name: /selected repositories/i,
|
||||
});
|
||||
|
||||
const selectedButton = await getSelectedButton();
|
||||
await user.click(selectedButton);
|
||||
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
|
||||
await clickNext();
|
||||
clickBack();
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
});
|
||||
|
||||
test('press on All button to see all repositories list', async () => {
|
||||
await setUp();
|
||||
|
||||
const getFirstRepoCheckbox = async () =>
|
||||
await screen.findByRole('checkbox', {
|
||||
name: /select row 0/i,
|
||||
});
|
||||
const firstRepoCheckbox = await getFirstRepoCheckbox();
|
||||
|
||||
const getSecondRepoCheckbox = async () =>
|
||||
await screen.findByRole('checkbox', {
|
||||
name: /select row 1/i,
|
||||
});
|
||||
const secondRepoCheckbox = await getSecondRepoCheckbox();
|
||||
|
||||
expect(firstRepoCheckbox.checked).toEqual(false);
|
||||
expect(secondRepoCheckbox.checked).toEqual(false);
|
||||
await user.click(firstRepoCheckbox);
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
expect(secondRepoCheckbox.checked).toEqual(false);
|
||||
|
||||
const getAllButton = async () =>
|
||||
await screen.findByRole('button', {
|
||||
name: /all repositories/i,
|
||||
});
|
||||
|
||||
const allButton = await getAllButton();
|
||||
await user.click(allButton);
|
||||
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
expect(secondRepoCheckbox.checked).toEqual(false);
|
||||
|
||||
await clickNext();
|
||||
clickBack();
|
||||
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
expect(secondRepoCheckbox.checked).toEqual(false);
|
||||
});
|
||||
|
||||
test('press on Selected button to see selected repositories list at the second page and filter checked repo', async () => {
|
||||
await setUp();
|
||||
|
||||
const getFirstRepoCheckbox = async () =>
|
||||
await screen.findByRole('checkbox', {
|
||||
name: /select row 0/i,
|
||||
});
|
||||
|
||||
const firstRepoCheckbox = await getFirstRepoCheckbox();
|
||||
|
||||
const getNextPageButton = async () =>
|
||||
await screen.findAllByRole('button', {
|
||||
name: /go to next page/i,
|
||||
});
|
||||
|
||||
const nextPageButton = await getNextPageButton();
|
||||
|
||||
expect(firstRepoCheckbox.checked).toEqual(false);
|
||||
await user.click(firstRepoCheckbox);
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
|
||||
await user.click(nextPageButton[0]);
|
||||
|
||||
const getSelectedButton = async () =>
|
||||
await screen.findByRole('button', {
|
||||
name: /selected repositories/i,
|
||||
});
|
||||
|
||||
const selectedButton = await getSelectedButton();
|
||||
await user.click(selectedButton);
|
||||
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
|
||||
await user.type(
|
||||
await screen.findByRole('textbox', { name: /search repositories/i }),
|
||||
'13lk3'
|
||||
);
|
||||
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
|
||||
await clickNext();
|
||||
clickBack();
|
||||
expect(firstRepoCheckbox.checked).toEqual(true);
|
||||
await user.click(firstRepoCheckbox);
|
||||
expect(firstRepoCheckbox.checked).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('On Recreate', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
renderWithReduxRouter('imagewizard/hyk93673-8dcc-4a61-ac30-e9f4940d8346');
|
||||
};
|
||||
|
||||
const setUpUnavailableRepo = async () => {
|
||||
renderWithReduxRouter('imagewizard/b7193673-8dcc-4a5f-ac30-e9f4940d8346');
|
||||
};
|
||||
|
||||
test('with valid repositories', async () => {
|
||||
await setUp();
|
||||
|
||||
await screen.findByRole('heading', { name: /review/i });
|
||||
expect(
|
||||
screen.queryByText('Previously added custom repository unavailable')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const createImageButton = await screen.findByRole('button', {
|
||||
name: /create image/i,
|
||||
});
|
||||
await waitFor(() => expect(createImageButton).toBeEnabled());
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: /custom repositories/i })
|
||||
);
|
||||
|
||||
await screen.findByRole('heading', { name: /custom repositories/i });
|
||||
expect(
|
||||
screen.queryByText('Previously added custom repository unavailable')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const table = await screen.findByTestId('repositories-table');
|
||||
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
|
||||
const availableRepo = rows[1].cells[1];
|
||||
expect(availableRepo).toHaveTextContent(
|
||||
'13lk3http://yum.theforeman.org/releases/3.4/el8/x86_64/'
|
||||
);
|
||||
|
||||
const availableRepoCheckbox = await screen.findByRole('checkbox', {
|
||||
name: /select row 0/i,
|
||||
});
|
||||
await waitFor(() => expect(availableRepoCheckbox).toBeEnabled());
|
||||
});
|
||||
|
||||
test('with repositories that are no longer available', async () => {
|
||||
await setUpUnavailableRepo();
|
||||
|
||||
await screen.findByRole('heading', { name: /review/i });
|
||||
await screen.findByText('Previously added custom repository unavailable');
|
||||
|
||||
const createImageButton = await screen.findByRole('button', {
|
||||
name: /create image/i,
|
||||
});
|
||||
expect(createImageButton).toBeDisabled();
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: /custom repositories/i })
|
||||
);
|
||||
|
||||
await screen.findByRole('heading', { name: /custom repositories/i });
|
||||
await screen.findByText('Previously added custom repository unavailable');
|
||||
|
||||
const table = await screen.findByTestId('repositories-table');
|
||||
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
|
||||
const unavailableRepo = rows[1].cells[1];
|
||||
expect(unavailableRepo).toHaveTextContent(
|
||||
'Repository with the following url is no longer available:http://unreachable.link.to.repo.org/x86_64/'
|
||||
);
|
||||
|
||||
const unavailableRepoCheckbox = await screen.findByRole('checkbox', {
|
||||
name: /select row 0/i,
|
||||
});
|
||||
expect(unavailableRepoCheckbox).toBeDisabled();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,260 +0,0 @@
|
|||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import CreateImageWizard from '../../../../Components/CreateImageWizard/CreateImageWizard';
|
||||
import { AARCH64, RHEL_8, RHEL_9, X86_64 } from '../../../../constants';
|
||||
import { mockArchitecturesByDistro } from '../../../fixtures/architectures';
|
||||
import { server } from '../../../mocks/server';
|
||||
import { renderCustomRoutesWithReduxRouter } from '../../../testUtils';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: 'insights/image-builder/imagewizard/:composeId?',
|
||||
element: <CreateImageWizard />,
|
||||
},
|
||||
];
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
useChrome: () => ({
|
||||
auth: {
|
||||
getUser: () => {
|
||||
return {
|
||||
identity: {
|
||||
internal: {
|
||||
org_id: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
isBeta: () => true,
|
||||
isProd: () => true,
|
||||
getEnvironment: () => 'prod',
|
||||
}),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
// scrollTo is not defined in jsdom
|
||||
window.HTMLElement.prototype.scrollTo = function () {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('Check that the target filtering is in accordance to mock content', () => {
|
||||
test('rhel9 x86_64', async () => {
|
||||
const user = userEvent.setup();
|
||||
await renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
|
||||
|
||||
// select x86_64
|
||||
const archMenu = screen.getAllByRole('button', {
|
||||
name: /options menu/i,
|
||||
})[1];
|
||||
await user.click(archMenu);
|
||||
await user.click(await screen.findByRole('option', { name: 'x86_64' }));
|
||||
|
||||
// make sure this test is in SYNC with the mocks
|
||||
let images_types = [];
|
||||
mockArchitecturesByDistro(RHEL_9).forEach((elem) => {
|
||||
if (elem.arch === X86_64) {
|
||||
images_types = elem.image_types;
|
||||
}
|
||||
});
|
||||
expect(images_types).toContain('aws');
|
||||
expect(images_types).toContain('gcp');
|
||||
expect(images_types).toContain('azure');
|
||||
expect(images_types).toContain('guest-image');
|
||||
expect(images_types).toContain('image-installer');
|
||||
expect(images_types).toContain('vsphere');
|
||||
expect(images_types).toContain('vsphere-ova');
|
||||
expect(images_types).not.toContain('wsl');
|
||||
// make sure the UX conforms to the mocks
|
||||
await waitFor(async () => await screen.findByTestId('upload-aws'));
|
||||
await screen.findByTestId('upload-google');
|
||||
await screen.findByTestId('upload-azure');
|
||||
await screen.findByTestId('checkbox-guest-image');
|
||||
await screen.findByTestId('checkbox-image-installer');
|
||||
await screen.findByText(/vmware vsphere/i);
|
||||
await screen.findByText(/open virtualization format \(\.ova\)/i);
|
||||
expect(
|
||||
screen.queryByText(/wsl - windows subsystem for linux \(\.tar\.gz\)/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('rhel8 x86_64', async () => {
|
||||
const user = userEvent.setup();
|
||||
await renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
|
||||
|
||||
// select rhel8
|
||||
const releaseMenu = screen.getAllByRole('button', {
|
||||
name: /options menu/i,
|
||||
})[0];
|
||||
await user.click(releaseMenu);
|
||||
await user.click(
|
||||
await screen.findByRole('option', {
|
||||
name: /Red Hat Enterprise Linux \(RHEL\) 8/,
|
||||
})
|
||||
);
|
||||
|
||||
// select x86_64
|
||||
const archMenu = screen.getAllByRole('button', {
|
||||
name: /options menu/i,
|
||||
})[1];
|
||||
await user.click(archMenu);
|
||||
await user.click(await screen.findByRole('option', { name: 'x86_64' }));
|
||||
|
||||
// make sure this test is in SYNC with the mocks
|
||||
let images_types = [];
|
||||
mockArchitecturesByDistro(RHEL_8).forEach((elem) => {
|
||||
if (elem.arch === X86_64) {
|
||||
images_types = elem.image_types;
|
||||
}
|
||||
});
|
||||
expect(images_types).toContain('aws');
|
||||
expect(images_types).toContain('gcp');
|
||||
expect(images_types).toContain('azure');
|
||||
expect(images_types).toContain('guest-image');
|
||||
expect(images_types).toContain('image-installer');
|
||||
expect(images_types).toContain('vsphere');
|
||||
expect(images_types).toContain('vsphere-ova');
|
||||
expect(images_types).toContain('wsl');
|
||||
// make sure the UX conforms to the mocks
|
||||
await waitFor(async () => await screen.findByTestId('upload-aws'));
|
||||
await screen.findByTestId('upload-google');
|
||||
await screen.findByTestId('upload-azure');
|
||||
await screen.findByTestId('checkbox-guest-image');
|
||||
await screen.findByTestId('checkbox-image-installer');
|
||||
await screen.findByText(/vmware vsphere/i);
|
||||
await screen.findByText(/open virtualization format \(\.ova\)/i);
|
||||
await screen.findByText(/wsl - windows subsystem for linux \(\.tar\.gz\)/i);
|
||||
});
|
||||
|
||||
test('rhel9 aarch64', async () => {
|
||||
const user = userEvent.setup();
|
||||
await renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
|
||||
|
||||
// select aarch64
|
||||
const archMenu = screen.getAllByRole('button', {
|
||||
name: /options menu/i,
|
||||
})[1];
|
||||
await user.click(archMenu);
|
||||
await user.click(await screen.findByRole('option', { name: 'aarch64' }));
|
||||
|
||||
// make sure this test is in SYNC with the mocks
|
||||
let images_types = [];
|
||||
mockArchitecturesByDistro(RHEL_9).forEach((elem) => {
|
||||
if (elem.arch === AARCH64) {
|
||||
images_types = elem.image_types;
|
||||
}
|
||||
});
|
||||
expect(images_types).toContain('aws');
|
||||
expect(images_types).not.toContain('gcp');
|
||||
expect(images_types).not.toContain('azure');
|
||||
expect(images_types).toContain('guest-image');
|
||||
expect(images_types).toContain('image-installer');
|
||||
expect(images_types).not.toContain('vsphere');
|
||||
expect(images_types).not.toContain('vsphere-ova');
|
||||
expect(images_types).not.toContain('wsl');
|
||||
// make sure the UX conforms to the mocks
|
||||
await waitFor(async () => await screen.findByTestId('upload-aws'));
|
||||
expect(screen.queryByTestId('upload-google')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('upload-azure')).not.toBeInTheDocument();
|
||||
await screen.findByTestId('checkbox-guest-image');
|
||||
await screen.findByTestId('checkbox-image-installer');
|
||||
expect(screen.queryByText(/vmware vsphere/i)).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/open virtualization format \(\.ova\)/i)
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/wsl - windows subsystem for linux \(\.tar\.gz\)/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('rhel8 aarch64', async () => {
|
||||
const user = userEvent.setup();
|
||||
await renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
|
||||
|
||||
// select rhel8
|
||||
const releaseMenu = screen.getAllByRole('button', {
|
||||
name: /options menu/i,
|
||||
})[0];
|
||||
await user.click(releaseMenu);
|
||||
await user.click(
|
||||
await screen.findByRole('option', {
|
||||
name: /Red Hat Enterprise Linux \(RHEL\) 8/,
|
||||
})
|
||||
);
|
||||
|
||||
// select x86_64
|
||||
const archMenu = screen.getAllByRole('button', {
|
||||
name: /options menu/i,
|
||||
})[1];
|
||||
await user.click(archMenu);
|
||||
await user.click(await screen.findByRole('option', { name: 'aarch64' }));
|
||||
|
||||
// make sure this test is in SYNC with the mocks
|
||||
let images_types = [];
|
||||
mockArchitecturesByDistro(RHEL_8).forEach((elem) => {
|
||||
if (elem.arch === AARCH64) {
|
||||
images_types = elem.image_types;
|
||||
}
|
||||
});
|
||||
expect(images_types).toContain('aws');
|
||||
expect(images_types).not.toContain('gcp');
|
||||
expect(images_types).not.toContain('azure');
|
||||
expect(images_types).toContain('guest-image');
|
||||
expect(images_types).toContain('image-installer');
|
||||
expect(images_types).not.toContain('vsphere');
|
||||
expect(images_types).not.toContain('vsphere-ova');
|
||||
expect(images_types).not.toContain('wsl');
|
||||
// make sure the UX conforms to the mocks
|
||||
await waitFor(async () => await screen.findByTestId('upload-aws'));
|
||||
expect(screen.queryByTestId('upload-google')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('upload-azure')).not.toBeInTheDocument();
|
||||
await screen.findByTestId('checkbox-guest-image');
|
||||
await screen.findByTestId('checkbox-image-installer');
|
||||
expect(screen.queryByText(/vmware vsphere/i)).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/open virtualization format \(\.ova\)/i)
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/wsl - windows subsystem for linux \(\.tar\.gz\)/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Check step consistency', () => {
|
||||
test('going back and forth with selected options only keeps the one compatible', async () => {
|
||||
const user = userEvent.setup();
|
||||
await renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
|
||||
|
||||
// select x86_64
|
||||
const archMenu = screen.getAllByRole('button', {
|
||||
name: /options menu/i,
|
||||
})[1];
|
||||
await user.click(archMenu);
|
||||
await user.click(await screen.findByRole('option', { name: 'x86_64' }));
|
||||
await waitFor(async () => await screen.findByTestId('upload-aws'));
|
||||
// select GCP, it's available for x86_64
|
||||
await user.click(await screen.findByTestId('upload-google'));
|
||||
const next = await screen.findByRole('button', { name: /Next/ });
|
||||
await waitFor(() => expect(next).toBeEnabled());
|
||||
// Change to aarch
|
||||
await user.click(archMenu);
|
||||
await user.click(await screen.findByRole('option', { name: 'aarch64' }));
|
||||
await waitFor(async () => await screen.findByTestId('upload-aws'));
|
||||
// GCP not being compatible with arch, the next button is disabled
|
||||
await waitFor(() => expect(next).toBeDisabled());
|
||||
// clicking on AWS the user can go next
|
||||
await user.click(await screen.findByTestId('upload-aws'));
|
||||
await waitFor(() => expect(next).toBeEnabled());
|
||||
// and going back to x86_64 the user should keep the next button visible
|
||||
await user.click(archMenu);
|
||||
await user.click(await screen.findByRole('option', { name: 'x86_64' }));
|
||||
await waitFor(() => expect(next).toBeEnabled());
|
||||
});
|
||||
});
|
||||
|
|
@ -1066,7 +1066,7 @@ describe('Step Review', () => {
|
|||
test('has 3 buttons', async () => {
|
||||
await setUp();
|
||||
|
||||
await screen.findByRole('button', { name: /Create/ });
|
||||
await screen.findByRole('button', { name: /Create blueprint/ });
|
||||
await screen.findByRole('button', { name: /Back/ });
|
||||
await screen.findByRole('button', { name: /Cancel/ });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,10 +23,6 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
|||
switch (flag) {
|
||||
case 'edgeParity.image-list':
|
||||
return false;
|
||||
case 'image-builder.new-wizard.enabled':
|
||||
return false;
|
||||
case 'image-builder.new-wizard.stable':
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
|
@ -60,8 +56,9 @@ describe('Images Table', () => {
|
|||
expect(headerCells[2]).toHaveTextContent('Updated');
|
||||
expect(headerCells[3]).toHaveTextContent('OS');
|
||||
expect(headerCells[4]).toHaveTextContent('Target');
|
||||
expect(headerCells[5]).toHaveTextContent('Status');
|
||||
expect(headerCells[6]).toHaveTextContent('Instance');
|
||||
expect(headerCells[5]).toHaveTextContent('Version');
|
||||
expect(headerCells[6]).toHaveTextContent('Status');
|
||||
expect(headerCells[7]).toHaveTextContent('Instance');
|
||||
|
||||
const imageNameValues = mockComposes.map((compose) =>
|
||||
compose.image_name ? compose.image_name : compose.id
|
||||
|
|
@ -79,34 +76,6 @@ describe('Images Table', () => {
|
|||
// TODO Test remaining table content.
|
||||
});
|
||||
|
||||
test('check recreate action', async () => {
|
||||
const { router } = await renderWithReduxRouter('', {});
|
||||
|
||||
// get rows
|
||||
const table = await screen.findByTestId('images-table');
|
||||
const { findAllByRole } = within(table);
|
||||
const rows = await findAllByRole('row');
|
||||
|
||||
const actionsButton = await within(rows[1]).findByRole('button', {
|
||||
name: 'Kebab toggle',
|
||||
});
|
||||
|
||||
expect(actionsButton).toBeEnabled();
|
||||
|
||||
await user.click(actionsButton);
|
||||
const recreateButton = await screen.findByRole('menuitem', {
|
||||
name: 'Recreate image',
|
||||
});
|
||||
|
||||
await user.click(recreateButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(router.state.location.pathname).toBe(
|
||||
'/insights/image-builder/imagewizard/1579d95b-8f1d-4982-8c53-8c2afa4ab04c'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('check download compose request action', async () => {
|
||||
await renderWithReduxRouter('', {});
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
|||
switch (flag) {
|
||||
case 'edgeParity.image-list':
|
||||
return false;
|
||||
case 'image-builder.new-wizard.stable':
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
|
@ -33,7 +31,8 @@ describe('Landing Page', () => {
|
|||
renderWithReduxRouter('', {});
|
||||
|
||||
// check heading
|
||||
await screen.findByRole('heading', { name: /Images/i });
|
||||
const heading = await screen.findByText('Images');
|
||||
expect(heading).toHaveRole('heading');
|
||||
});
|
||||
|
||||
test('renders EmptyState child component', async () => {
|
||||
|
|
@ -44,9 +43,6 @@ describe('Landing Page', () => {
|
|||
);
|
||||
|
||||
renderWithReduxRouter('', {});
|
||||
|
||||
// check action loads
|
||||
await screen.findByTestId('create-image-action-empty-state');
|
||||
// check table loads
|
||||
await screen.findByTestId('empty-state');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import userEvent from '@testing-library/user-event';
|
|||
import { Provider } from 'react-redux';
|
||||
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import CreateImageWizard from '../Components/CreateImageWizard/CreateImageWizard';
|
||||
import ImportImageWizard from '../Components/CreateImageWizardV2/ImportImageWizard';
|
||||
import LandingPage from '../Components/LandingPage/LandingPage';
|
||||
import ShareImageModal from '../Components/ShareImageModal/ShareImageModal';
|
||||
import { middleware, reducer } from '../store';
|
||||
|
|
@ -44,8 +44,8 @@ export const renderWithReduxRouter = async (
|
|||
element: <LandingPage />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/imagewizard/:composeId?',
|
||||
element: <CreateImageWizard />,
|
||||
path: 'insights/image-builder/imagewizard/import',
|
||||
element: <ImportImageWizard />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/share/:composeId',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue