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
|
##### Mocking flags for tests
|
||||||
|
|
||||||
Flags can be mocked for the unit tests to access some feature. Checkout:
|
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
|
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
|
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;
|
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 = {
|
module.exports = {
|
||||||
...webpackConfig,
|
...webpackConfig,
|
||||||
plugins,
|
plugins,
|
||||||
|
|
|
||||||
|
|
@ -127,9 +127,7 @@
|
||||||
"prod-stable": "PROXY=true webpack serve --config config/dev.webpack.config.js",
|
"prod-stable": "PROXY=true webpack serve --config config/dev.webpack.config.js",
|
||||||
"stage-stable": "STAGE=true npm run prod-stable",
|
"stage-stable": "STAGE=true npm run prod-stable",
|
||||||
"stage-beta": "STAGE=true npm run prod-beta",
|
"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": "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": "TZ=UTC jest --verbose --no-cache",
|
||||||
"test:single": "jest --verbose -w 1",
|
"test:single": "jest --verbose -w 1",
|
||||||
"build": "webpack --config config/prod.webpack.config.js",
|
"build": "webpack --config config/prod.webpack.config.js",
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
||||||
switch (flag) {
|
switch (flag) {
|
||||||
case 'image-builder.import.enabled':
|
case 'image-builder.import.enabled':
|
||||||
return true;
|
return true;
|
||||||
case 'image-builder.new-wizard.enabled':
|
|
||||||
return true;
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -243,17 +241,15 @@ describe('Import model', () => {
|
||||||
expect(helperText).toBeInTheDocument();
|
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 setUp();
|
||||||
await uploadFile(`blueprints.json`, BLUEPRINT_JSON);
|
await uploadFile(`blueprints.json`, BLUEPRINT_JSON);
|
||||||
const reviewButton = screen.getByTestId('import-blueprint-finish');
|
const reviewButton = screen.getByTestId('import-blueprint-finish');
|
||||||
await waitFor(() => expect(reviewButton).not.toHaveClass('pf-m-disabled'));
|
await waitFor(() => expect(reviewButton).not.toHaveClass('pf-m-disabled'));
|
||||||
|
await user.click(reviewButton);
|
||||||
|
|
||||||
await userEvent.click(reviewButton);
|
expect(
|
||||||
await waitFor(() => {
|
await screen.findByText('Image output', { selector: 'h1' })
|
||||||
expect(
|
).toBeInTheDocument();
|
||||||
screen.getByRole('heading', { name: 'Image output' })
|
|
||||||
).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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ImageBuilderHeader />
|
<ImageBuilderHeader inWizard />
|
||||||
<section className="pf-l-page__main-section pf-c-page__main-section">
|
<section className="pf-l-page__main-section pf-c-page__main-section">
|
||||||
<Wizard
|
<Wizard
|
||||||
startIndex={startIndex}
|
startIndex={startIndex}
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,11 @@ import {
|
||||||
PlusCircleIcon,
|
PlusCircleIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CREATING_IMAGES_WITH_IB_SERVICE_URL,
|
CREATING_IMAGES_WITH_IB_SERVICE_URL,
|
||||||
MANAGING_WITH_DNF_URL,
|
MANAGING_WITH_DNF_URL,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { resolveRelPath } from '../../Utilities/path';
|
|
||||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
|
||||||
import { BuildImagesButton } from '../Blueprints/BuildImagesButton';
|
import { BuildImagesButton } from '../Blueprints/BuildImagesButton';
|
||||||
|
|
||||||
type ImagesEmptyStateProps = {
|
type ImagesEmptyStateProps = {
|
||||||
|
|
@ -57,84 +54,58 @@ const EmptyBlueprintsImagesTable = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const EmptyImagesTable = () => {
|
const EmptyImagesTable = () => {
|
||||||
const experimentalFlag = useExperimentalFlag();
|
|
||||||
return (
|
return (
|
||||||
<Bullseye>
|
<Bullseye>
|
||||||
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
|
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
|
||||||
{experimentalFlag ? (
|
<>
|
||||||
<>
|
<EmptyStateHeader
|
||||||
<EmptyStateHeader
|
titleText="No images"
|
||||||
titleText="No images"
|
icon={<EmptyStateIcon icon={SearchIcon} />}
|
||||||
icon={<EmptyStateIcon icon={SearchIcon} />}
|
headingLevel="h4"
|
||||||
headingLevel="h4"
|
/>
|
||||||
/>
|
<EmptyStateBody>
|
||||||
<EmptyStateBody>
|
<Text>
|
||||||
<Text>Images are BLANK. Create blueprints to create images.</Text>
|
Image builder is a tool for creating deployment-ready customized
|
||||||
</EmptyStateBody>
|
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
|
||||||
<EmptyStateHeader
|
type.
|
||||||
titleText="Create an RPM-DNF image"
|
</Text>
|
||||||
icon={<EmptyStateIcon icon={PlusCircleIcon} />}
|
<Text>
|
||||||
headingLevel="h4"
|
There are no images yet. Create a blueprint to create images.
|
||||||
/>
|
</Text>
|
||||||
<EmptyStateBody>
|
<Text>
|
||||||
<Text>
|
<Button
|
||||||
Image builder is a tool for creating deployment-ready customized
|
component="a"
|
||||||
system images: installation disks, virtual machines, cloud
|
target="_blank"
|
||||||
vendor-specific images, and others. By using image builder, you
|
variant="link"
|
||||||
can create these images faster than with manual procedures
|
icon={<ExternalLinkAltIcon />}
|
||||||
because it eliminates the specific configurations required for
|
iconPosition="right"
|
||||||
each output type.
|
isInline
|
||||||
</Text>
|
href={MANAGING_WITH_DNF_URL}
|
||||||
<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"
|
|
||||||
>
|
>
|
||||||
Create image
|
Learn more about managing images with DNF
|
||||||
</Link>
|
</Button>
|
||||||
<EmptyStateActions>
|
</Text>
|
||||||
<Button
|
</EmptyStateBody>
|
||||||
component="a"
|
<EmptyStateFooter>
|
||||||
target="_blank"
|
<EmptyStateActions>
|
||||||
variant="link"
|
<Button
|
||||||
icon={<ExternalLinkAltIcon />}
|
component="a"
|
||||||
iconPosition="right"
|
target="_blank"
|
||||||
isInline
|
variant="link"
|
||||||
href={CREATING_IMAGES_WITH_IB_SERVICE_URL}
|
icon={<ExternalLinkAltIcon />}
|
||||||
className="pf-u-pt-md"
|
iconPosition="right"
|
||||||
>
|
isInline
|
||||||
Image builder for RPM-DNF documentation
|
href={CREATING_IMAGES_WITH_IB_SERVICE_URL}
|
||||||
</Button>
|
className="pf-u-pt-md"
|
||||||
</EmptyStateActions>
|
>
|
||||||
</EmptyStateFooter>
|
Image builder for RPM-DNF documentation
|
||||||
</>
|
</Button>
|
||||||
)}
|
</EmptyStateActions>
|
||||||
|
</EmptyStateFooter>
|
||||||
|
</>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
</Bullseye>
|
</Bullseye>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ import {
|
||||||
timestampToDisplayString,
|
timestampToDisplayString,
|
||||||
timestampToDisplayStringDetailed,
|
timestampToDisplayStringDetailed,
|
||||||
} from '../../Utilities/time';
|
} from '../../Utilities/time';
|
||||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
|
||||||
|
|
||||||
const ImagesTable = () => {
|
const ImagesTable = () => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
@ -93,7 +92,6 @@ const ImagesTable = () => {
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const experimentalFlag = useExperimentalFlag();
|
|
||||||
const onSetPage: OnSetPage = (_, page) => setPage(page);
|
const onSetPage: OnSetPage = (_, page) => setPage(page);
|
||||||
|
|
||||||
const onPerPageSelect: OnSetPage = (_, perPage) => {
|
const onPerPageSelect: OnSetPage = (_, perPage) => {
|
||||||
|
|
@ -198,7 +196,7 @@ const ImagesTable = () => {
|
||||||
<Th>Updated</Th>
|
<Th>Updated</Th>
|
||||||
<Th>OS</Th>
|
<Th>OS</Th>
|
||||||
<Th>Target</Th>
|
<Th>Target</Th>
|
||||||
{experimentalFlag && <Th>Version</Th>}
|
<Th>Version</Th>
|
||||||
<Th>Status</Th>
|
<Th>Status</Th>
|
||||||
<Th>Instance</Th>
|
<Th>Instance</Th>
|
||||||
<Th aria-label="Actions menu" />
|
<Th aria-label="Actions menu" />
|
||||||
|
|
@ -407,7 +405,6 @@ type AwsRowPropTypes = {
|
||||||
|
|
||||||
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const experimentalFlag = useExperimentalFlag();
|
|
||||||
|
|
||||||
const target = <AwsTarget compose={compose} />;
|
const target = <AwsTarget compose={compose} />;
|
||||||
|
|
||||||
|
|
@ -418,9 +415,7 @@ const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
||||||
const details = <AwsDetails compose={compose} />;
|
const details = <AwsDetails compose={compose} />;
|
||||||
|
|
||||||
const actions = (
|
const actions = (
|
||||||
<ActionsColumn
|
<ActionsColumn items={awsActions(compose, composeStatus, navigate)} />
|
||||||
items={awsActions(compose, composeStatus, navigate, experimentalFlag)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -457,8 +452,6 @@ const Row = ({
|
||||||
}: RowPropTypes) => {
|
}: RowPropTypes) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const handleToggle = () => setIsExpanded(!isExpanded);
|
const handleToggle = () => setIsExpanded(!isExpanded);
|
||||||
const experimentalFlag = useExperimentalFlag();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tbody key={compose.id} isExpanded={isExpanded}>
|
<Tbody key={compose.id} isExpanded={isExpanded}>
|
||||||
|
|
@ -483,20 +476,16 @@ const Row = ({
|
||||||
<Td dataLabel="Target">
|
<Td dataLabel="Target">
|
||||||
{target ? target : <Target compose={compose} />}
|
{target ? target : <Target compose={compose} />}
|
||||||
</Td>
|
</Td>
|
||||||
{experimentalFlag && (
|
<Td dataLabel="Version">
|
||||||
<Td dataLabel="Version">
|
<Badge isRead>{compose.blueprint_version || 'N/A'}</Badge>
|
||||||
<Badge isRead>{compose.blueprint_version || 'N/A'}</Badge>
|
</Td>
|
||||||
</Td>
|
|
||||||
)}
|
|
||||||
<Td dataLabel="Status">{status}</Td>
|
<Td dataLabel="Status">{status}</Td>
|
||||||
<Td dataLabel="Instance">{instance}</Td>
|
<Td dataLabel="Instance">{instance}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{actions ? (
|
{actions ? (
|
||||||
actions
|
actions
|
||||||
) : (
|
) : (
|
||||||
<ActionsColumn
|
<ActionsColumn items={defaultActions(compose)} />
|
||||||
items={defaultActions(compose, navigate, experimentalFlag)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|
@ -509,21 +498,7 @@ const Row = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultActions = (
|
const defaultActions = (compose: ComposesResponseItem) => [
|
||||||
compose: ComposesResponseItem,
|
|
||||||
navigate: NavigateFunction,
|
|
||||||
experimentalFlag: boolean
|
|
||||||
) => [
|
|
||||||
...(experimentalFlag
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
title: 'Recreate image',
|
|
||||||
onClick: () => {
|
|
||||||
navigate(resolveRelPath(`imagewizard/${compose.id}`));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
{
|
{
|
||||||
title: (
|
title: (
|
||||||
<a
|
<a
|
||||||
|
|
@ -542,15 +517,14 @@ const defaultActions = (
|
||||||
const awsActions = (
|
const awsActions = (
|
||||||
compose: ComposesResponseItem,
|
compose: ComposesResponseItem,
|
||||||
status: ComposeStatus | undefined,
|
status: ComposeStatus | undefined,
|
||||||
navigate: NavigateFunction,
|
navigate: NavigateFunction
|
||||||
experimentalFlag: boolean
|
|
||||||
) => [
|
) => [
|
||||||
{
|
{
|
||||||
title: 'Share to new region',
|
title: 'Share to new region',
|
||||||
onClick: () => navigate(resolveRelPath(`share/${compose.id}`)),
|
onClick: () => navigate(resolveRelPath(`share/${compose.id}`)),
|
||||||
isDisabled: status?.image_status.status === 'success' ? false : true,
|
isDisabled: status?.image_status.status === 'success' ? false : true,
|
||||||
},
|
},
|
||||||
...defaultActions(compose, navigate, experimentalFlag),
|
...defaultActions(compose),
|
||||||
];
|
];
|
||||||
|
|
||||||
export default ImagesTable;
|
export default ImagesTable;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
Title,
|
Title,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
selectSelectedBlueprintId,
|
selectSelectedBlueprintId,
|
||||||
|
|
@ -21,8 +20,6 @@ import {
|
||||||
useGetBlueprintsQuery,
|
useGetBlueprintsQuery,
|
||||||
useGetBlueprintComposesQuery,
|
useGetBlueprintComposesQuery,
|
||||||
} from '../../store/imageBuilderApi';
|
} from '../../store/imageBuilderApi';
|
||||||
import { resolveRelPath } from '../../Utilities/path';
|
|
||||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
|
||||||
import { BlueprintActionsMenu } from '../Blueprints/BlueprintActionsMenu';
|
import { BlueprintActionsMenu } from '../Blueprints/BlueprintActionsMenu';
|
||||||
import BlueprintVersionFilter from '../Blueprints/BlueprintVersionFilter';
|
import BlueprintVersionFilter from '../Blueprints/BlueprintVersionFilter';
|
||||||
import { BuildImagesButton } from '../Blueprints/BuildImagesButton';
|
import { BuildImagesButton } from '../Blueprints/BuildImagesButton';
|
||||||
|
|
@ -44,7 +41,6 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
||||||
setPage,
|
setPage,
|
||||||
onPerPageSelect,
|
onPerPageSelect,
|
||||||
}: imagesTableToolbarProps) => {
|
}: imagesTableToolbarProps) => {
|
||||||
const experimentalFlag = useExperimentalFlag();
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||||
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
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 = () => {
|
const isBlueprintDistroCentos8 = () => {
|
||||||
if (isSuccessBlueprintsCompose) {
|
if (isSuccessBlueprintsCompose) {
|
||||||
return blueprintsComposes.data[0].request.distribution === 'centos-8';
|
return blueprintsComposes.data[0].request.distribution === 'centos-8';
|
||||||
|
|
@ -139,7 +114,7 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
||||||
: 'All images'}
|
: 'All images'}
|
||||||
</Title>
|
</Title>
|
||||||
</ToolbarContent>
|
</ToolbarContent>
|
||||||
{itemCount > 0 && experimentalFlag && isBlueprintOutSync && (
|
{itemCount > 0 && isBlueprintOutSync && (
|
||||||
<Alert
|
<Alert
|
||||||
style={{
|
style={{
|
||||||
margin:
|
margin:
|
||||||
|
|
@ -164,25 +139,28 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
||||||
ouiaId="centos-8-blueprint-alert"
|
ouiaId="centos-8-blueprint-alert"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{selectedBlueprintId && (
|
|
||||||
<ToolbarContent>
|
<ToolbarContent>
|
||||||
<ToolbarItem>
|
{selectedBlueprintId && (
|
||||||
<BlueprintVersionFilter onFilterChange={() => setPage(1)} />
|
<>
|
||||||
</ToolbarItem>
|
<ToolbarItem>
|
||||||
<ToolbarItem>
|
<BlueprintVersionFilter onFilterChange={() => setPage(1)} />
|
||||||
<BuildImagesButton />
|
</ToolbarItem>
|
||||||
</ToolbarItem>
|
<ToolbarItem>
|
||||||
<ToolbarItem>
|
<BuildImagesButton />
|
||||||
<EditBlueprintButton />
|
</ToolbarItem>
|
||||||
</ToolbarItem>
|
<ToolbarItem>
|
||||||
<ToolbarItem>
|
<EditBlueprintButton />
|
||||||
<BlueprintActionsMenu setShowDeleteModal={setShowDeleteModal} />
|
</ToolbarItem>
|
||||||
</ToolbarItem>
|
<ToolbarItem>
|
||||||
<ToolbarItem variant="pagination" align={{ default: 'alignRight' }}>
|
<BlueprintActionsMenu setShowDeleteModal={setShowDeleteModal} />
|
||||||
{pagination}
|
</ToolbarItem>
|
||||||
</ToolbarItem>
|
</>
|
||||||
</ToolbarContent>
|
)}
|
||||||
)}
|
<ToolbarItem variant="pagination" align={{ default: 'alignRight' }}>
|
||||||
|
{pagination}
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarContent>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ import {
|
||||||
isOciUploadStatus,
|
isOciUploadStatus,
|
||||||
} from '../../store/typeGuards';
|
} from '../../store/typeGuards';
|
||||||
import { resolveRelPath } from '../../Utilities/path';
|
import { resolveRelPath } from '../../Utilities/path';
|
||||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
|
||||||
import useProvisioningPermissions from '../../Utilities/useProvisioningPermissions';
|
import useProvisioningPermissions from '../../Utilities/useProvisioningPermissions';
|
||||||
|
|
||||||
type CloudInstancePropTypes = {
|
type CloudInstancePropTypes = {
|
||||||
|
|
@ -347,9 +346,6 @@ export const AwsS3Instance = ({
|
||||||
composeId: compose.id,
|
composeId: compose.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const experimentalFlag = useExperimentalFlag();
|
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
return <Skeleton />;
|
return <Skeleton />;
|
||||||
}
|
}
|
||||||
|
|
@ -388,18 +384,6 @@ export const AwsS3Instance = ({
|
||||||
)
|
)
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
} else if (isExpired && !experimentalFlag) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
target="_blank"
|
|
||||||
variant="link"
|
|
||||||
onClick={() => navigate(resolveRelPath(`imagewizard/${compose.id}`))}
|
|
||||||
isInline
|
|
||||||
>
|
|
||||||
Recreate image
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,10 @@ import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import './LandingPage.scss';
|
import './LandingPage.scss';
|
||||||
|
|
||||||
import { NewAlert } from './NewAlert';
|
import { NewAlert } from './NewAlert';
|
||||||
import Quickstarts from './Quickstarts';
|
|
||||||
|
|
||||||
import { MANAGING_WITH_DNF_URL, OSTREE_URL } from '../../constants';
|
import { MANAGING_WITH_DNF_URL, OSTREE_URL } from '../../constants';
|
||||||
import { manageEdgeImagesUrlName } from '../../Utilities/edge';
|
import { manageEdgeImagesUrlName } from '../../Utilities/edge';
|
||||||
import { resolveRelPath } from '../../Utilities/path';
|
import { resolveRelPath } from '../../Utilities/path';
|
||||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
|
||||||
import BlueprintsSidebar from '../Blueprints/BlueprintsSideBar';
|
import BlueprintsSidebar from '../Blueprints/BlueprintsSideBar';
|
||||||
import EdgeImagesTable from '../edge/ImagesTable';
|
import EdgeImagesTable from '../edge/ImagesTable';
|
||||||
import ImagesTable from '../ImagesTable/ImagesTable';
|
import ImagesTable from '../ImagesTable/ImagesTable';
|
||||||
|
|
@ -58,20 +56,8 @@ export const LandingPage = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const edgeParityFlag = useFlag('edgeParity.image-list');
|
const edgeParityFlag = useFlag('edgeParity.image-list');
|
||||||
const experimentalFlag = useExperimentalFlag();
|
|
||||||
|
|
||||||
const traditionalImageList = (
|
const imageList = (
|
||||||
<>
|
|
||||||
<PageSection>
|
|
||||||
<Quickstarts />
|
|
||||||
</PageSection>
|
|
||||||
<PageSection>
|
|
||||||
<ImagesTable />
|
|
||||||
</PageSection>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const experimentalImageList = (
|
|
||||||
<>
|
<>
|
||||||
<PageSection isWidthLimited>
|
<PageSection isWidthLimited>
|
||||||
{showAlert && <NewAlert setShowAlert={setShowAlert} />}
|
{showAlert && <NewAlert setShowAlert={setShowAlert} />}
|
||||||
|
|
@ -98,16 +84,9 @@ export const LandingPage = () => {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageList = experimentalFlag
|
|
||||||
? experimentalImageList
|
|
||||||
: traditionalImageList;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ImageBuilderHeader
|
<ImageBuilderHeader activeTab={activeTabKey} />
|
||||||
experimentalFlag={experimentalFlag}
|
|
||||||
activeTab={activeTabKey}
|
|
||||||
/>
|
|
||||||
{edgeParityFlag ? (
|
{edgeParityFlag ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
className="pf-c-tabs pf-c-page-header pf-c-table"
|
className="pf-c-tabs pf-c-page-header pf-c-table"
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ import './ImageBuilderHeader.scss';
|
||||||
import { ImportBlueprintModal } from '../Blueprints/ImportBlueprintModal';
|
import { ImportBlueprintModal } from '../Blueprints/ImportBlueprintModal';
|
||||||
|
|
||||||
type ImageBuilderHeaderPropTypes = {
|
type ImageBuilderHeaderPropTypes = {
|
||||||
experimentalFlag?: boolean;
|
|
||||||
activeTab?: number;
|
activeTab?: number;
|
||||||
|
inWizard?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AboutImageBuilderPopover = () => {
|
const AboutImageBuilderPopover = () => {
|
||||||
|
|
@ -92,8 +92,8 @@ const AboutImageBuilderPopover = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageBuilderHeader = ({
|
export const ImageBuilderHeader = ({
|
||||||
experimentalFlag,
|
|
||||||
activeTab,
|
activeTab,
|
||||||
|
inWizard,
|
||||||
}: ImageBuilderHeaderPropTypes) => {
|
}: ImageBuilderHeaderPropTypes) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const importExportFlag = useFlag('image-builder.import.enabled');
|
const importExportFlag = useFlag('image-builder.import.enabled');
|
||||||
|
|
@ -122,7 +122,7 @@ export const ImageBuilderHeader = ({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
{experimentalFlag && (
|
{!inWizard && (
|
||||||
<>
|
<>
|
||||||
<FlexItem align={{ default: 'alignRight' }}>
|
<FlexItem align={{ default: 'alignRight' }}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,8 @@ import { Route, Routes } from 'react-router-dom';
|
||||||
import EdgeImageDetail from './Components/edge/ImageDetails';
|
import EdgeImageDetail from './Components/edge/ImageDetails';
|
||||||
import ShareImageModal from './Components/ShareImageModal/ShareImageModal';
|
import ShareImageModal from './Components/ShareImageModal/ShareImageModal';
|
||||||
import { manageEdgeImagesUrlName } from './Utilities/edge';
|
import { manageEdgeImagesUrlName } from './Utilities/edge';
|
||||||
import { useExperimentalFlag } from './Utilities/useExperimentalFlag';
|
|
||||||
|
|
||||||
const LandingPage = lazy(() => import('./Components/LandingPage/LandingPage'));
|
const LandingPage = lazy(() => import('./Components/LandingPage/LandingPage'));
|
||||||
const CreateImageWizard = lazy(
|
|
||||||
() => import('./Components/CreateImageWizard/CreateImageWizard')
|
|
||||||
);
|
|
||||||
const ImportImageWizard = lazy(
|
const ImportImageWizard = lazy(
|
||||||
() => import('./Components/CreateImageWizardV2/ImportImageWizard')
|
() => import('./Components/CreateImageWizardV2/ImportImageWizard')
|
||||||
);
|
);
|
||||||
|
|
@ -22,7 +18,6 @@ const CreateImageWizardV2 = lazy(
|
||||||
export const Router = () => {
|
export const Router = () => {
|
||||||
const edgeParityFlag = useFlag('edgeParity.image-list');
|
const edgeParityFlag = useFlag('edgeParity.image-list');
|
||||||
const importExportFlag = useFlag('image-builder.import.enabled');
|
const importExportFlag = useFlag('image-builder.import.enabled');
|
||||||
const experimentalFlag = useExperimentalFlag();
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
|
|
@ -36,7 +31,7 @@ export const Router = () => {
|
||||||
<Route path="share/:composeId" element={<ShareImageModal />} />
|
<Route path="share/:composeId" element={<ShareImageModal />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{importExportFlag && experimentalFlag && (
|
{importExportFlag && (
|
||||||
<Route
|
<Route
|
||||||
path="imagewizard/import"
|
path="imagewizard/import"
|
||||||
element={
|
element={
|
||||||
|
|
@ -50,7 +45,7 @@ export const Router = () => {
|
||||||
path="imagewizard/:composeId?"
|
path="imagewizard/:composeId?"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{experimentalFlag ? <CreateImageWizardV2 /> : <CreateImageWizard />}
|
<CreateImageWizardV2 />
|
||||||
</Suspense>
|
</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 () => {
|
test('has 3 buttons', async () => {
|
||||||
await setUp();
|
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: /Back/ });
|
||||||
await screen.findByRole('button', { name: /Cancel/ });
|
await screen.findByRole('button', { name: /Cancel/ });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,6 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
||||||
switch (flag) {
|
switch (flag) {
|
||||||
case 'edgeParity.image-list':
|
case 'edgeParity.image-list':
|
||||||
return false;
|
return false;
|
||||||
case 'image-builder.new-wizard.enabled':
|
|
||||||
return false;
|
|
||||||
case 'image-builder.new-wizard.stable':
|
|
||||||
return false;
|
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -60,8 +56,9 @@ describe('Images Table', () => {
|
||||||
expect(headerCells[2]).toHaveTextContent('Updated');
|
expect(headerCells[2]).toHaveTextContent('Updated');
|
||||||
expect(headerCells[3]).toHaveTextContent('OS');
|
expect(headerCells[3]).toHaveTextContent('OS');
|
||||||
expect(headerCells[4]).toHaveTextContent('Target');
|
expect(headerCells[4]).toHaveTextContent('Target');
|
||||||
expect(headerCells[5]).toHaveTextContent('Status');
|
expect(headerCells[5]).toHaveTextContent('Version');
|
||||||
expect(headerCells[6]).toHaveTextContent('Instance');
|
expect(headerCells[6]).toHaveTextContent('Status');
|
||||||
|
expect(headerCells[7]).toHaveTextContent('Instance');
|
||||||
|
|
||||||
const imageNameValues = mockComposes.map((compose) =>
|
const imageNameValues = mockComposes.map((compose) =>
|
||||||
compose.image_name ? compose.image_name : compose.id
|
compose.image_name ? compose.image_name : compose.id
|
||||||
|
|
@ -79,34 +76,6 @@ describe('Images Table', () => {
|
||||||
// TODO Test remaining table content.
|
// 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 () => {
|
test('check download compose request action', async () => {
|
||||||
await renderWithReduxRouter('', {});
|
await renderWithReduxRouter('', {});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
||||||
switch (flag) {
|
switch (flag) {
|
||||||
case 'edgeParity.image-list':
|
case 'edgeParity.image-list':
|
||||||
return false;
|
return false;
|
||||||
case 'image-builder.new-wizard.stable':
|
|
||||||
return false;
|
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +31,8 @@ describe('Landing Page', () => {
|
||||||
renderWithReduxRouter('', {});
|
renderWithReduxRouter('', {});
|
||||||
|
|
||||||
// check heading
|
// 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 () => {
|
test('renders EmptyState child component', async () => {
|
||||||
|
|
@ -44,9 +43,6 @@ describe('Landing Page', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
renderWithReduxRouter('', {});
|
renderWithReduxRouter('', {});
|
||||||
|
|
||||||
// check action loads
|
|
||||||
await screen.findByTestId('create-image-action-empty-state');
|
|
||||||
// check table loads
|
// check table loads
|
||||||
await screen.findByTestId('empty-state');
|
await screen.findByTestId('empty-state');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import userEvent from '@testing-library/user-event';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
|
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 LandingPage from '../Components/LandingPage/LandingPage';
|
||||||
import ShareImageModal from '../Components/ShareImageModal/ShareImageModal';
|
import ShareImageModal from '../Components/ShareImageModal/ShareImageModal';
|
||||||
import { middleware, reducer } from '../store';
|
import { middleware, reducer } from '../store';
|
||||||
|
|
@ -44,8 +44,8 @@ export const renderWithReduxRouter = async (
|
||||||
element: <LandingPage />,
|
element: <LandingPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'insights/image-builder/imagewizard/:composeId?',
|
path: 'insights/image-builder/imagewizard/import',
|
||||||
element: <CreateImageWizard />,
|
element: <ImportImageWizard />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'insights/image-builder/share/:composeId',
|
path: 'insights/image-builder/share/:composeId',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue