update style across the project
The eslint updates require style changes in all components.
This commit is contained in:
parent
7959f2a563
commit
4fa71cede8
56 changed files with 5973 additions and 5177 deletions
|
|
@ -4,38 +4,46 @@ const { GitRevisionPlugin } = require('git-revision-webpack-plugin');
|
|||
const config = require('@redhat-cloud-services/frontend-components-config');
|
||||
|
||||
const webpackProxy = {
|
||||
useProxy: true,
|
||||
proxyVerbose: true,
|
||||
env: `${process.env.STAGE ? 'stage' : 'prod'}-${process.env.BETA ? 'beta' : 'stable'}`,
|
||||
appUrl: process.env.BETA ? '/beta/insights/image-builder' : '/insights/image-builder',
|
||||
useProxy: true,
|
||||
proxyVerbose: true,
|
||||
env: `${process.env.STAGE ? 'stage' : 'prod'}-${
|
||||
process.env.BETA ? 'beta' : 'stable'
|
||||
}`,
|
||||
appUrl: process.env.BETA
|
||||
? '/beta/insights/image-builder'
|
||||
: '/insights/image-builder',
|
||||
};
|
||||
|
||||
const { config: webpackConfig, plugins } = config({
|
||||
rootFolder: resolve(__dirname, '../'),
|
||||
debug: true,
|
||||
modules: [ 'image_builder' ],
|
||||
useFileHash: false,
|
||||
sassPrefix: '.imageBuilder, .image_builder',
|
||||
deployment: process.env.BETA ? 'beta/apps' : 'apps',
|
||||
...(process.env.PROXY ? webpackProxy : {})
|
||||
rootFolder: resolve(__dirname, '../'),
|
||||
debug: true,
|
||||
modules: ['image_builder'],
|
||||
useFileHash: false,
|
||||
sassPrefix: '.imageBuilder, .image_builder',
|
||||
deployment: process.env.BETA ? 'beta/apps' : 'apps',
|
||||
...(process.env.PROXY ? webpackProxy : {}),
|
||||
});
|
||||
|
||||
plugins.push(
|
||||
require('@redhat-cloud-services/frontend-components-config/federated-modules')({
|
||||
root: resolve(__dirname, '../'),
|
||||
useFileHash: false,
|
||||
moduleName: 'image_builder',
|
||||
exposes: {
|
||||
'./RootApp': resolve(__dirname, '../src/AppEntry.js'),
|
||||
},
|
||||
})
|
||||
require('@redhat-cloud-services/frontend-components-config/federated-modules')(
|
||||
{
|
||||
root: resolve(__dirname, '../'),
|
||||
useFileHash: false,
|
||||
moduleName: 'image_builder',
|
||||
exposes: {
|
||||
'./RootApp': resolve(__dirname, '../src/AppEntry.js'),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
plugins.push(new DefinePlugin({
|
||||
COMMITHASH: JSON.stringify((new GitRevisionPlugin()).commithash()),
|
||||
}));
|
||||
plugins.push(
|
||||
new DefinePlugin({
|
||||
COMMITHASH: JSON.stringify(new GitRevisionPlugin().commithash()),
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
...webpackConfig,
|
||||
plugins
|
||||
...webpackConfig,
|
||||
plugins,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,26 +3,30 @@ const { resolve } = require('path');
|
|||
const { GitRevisionPlugin } = require('git-revision-webpack-plugin');
|
||||
const config = require('@redhat-cloud-services/frontend-components-config');
|
||||
const { config: webpackConfig, plugins } = config({
|
||||
rootFolder: resolve(__dirname, '../'),
|
||||
modules: [ 'image_builder' ],
|
||||
sassPrefix: '.imageBuilder, .image_builder',
|
||||
rootFolder: resolve(__dirname, '../'),
|
||||
modules: ['image_builder'],
|
||||
sassPrefix: '.imageBuilder, .image_builder',
|
||||
});
|
||||
|
||||
plugins.push(
|
||||
require('@redhat-cloud-services/frontend-components-config/federated-modules')({
|
||||
root: resolve(__dirname, '../'),
|
||||
moduleName: 'image_builder',
|
||||
exposes: {
|
||||
'./RootApp': resolve(__dirname, '../src/AppEntry.js'),
|
||||
},
|
||||
})
|
||||
require('@redhat-cloud-services/frontend-components-config/federated-modules')(
|
||||
{
|
||||
root: resolve(__dirname, '../'),
|
||||
moduleName: 'image_builder',
|
||||
exposes: {
|
||||
'./RootApp': resolve(__dirname, '../src/AppEntry.js'),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
plugins.push(new DefinePlugin({
|
||||
COMMITHASH: JSON.stringify((new GitRevisionPlugin()).commithash()),
|
||||
}));
|
||||
plugins.push(
|
||||
new DefinePlugin({
|
||||
COMMITHASH: JSON.stringify(new GitRevisionPlugin().commithash()),
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
...webpackConfig,
|
||||
plugins
|
||||
...webpackConfig,
|
||||
plugins,
|
||||
};
|
||||
|
|
|
|||
40
src/App.js
40
src/App.js
|
|
@ -8,28 +8,28 @@ import NotificationsPortal from '@redhat-cloud-services/frontend-components-noti
|
|||
import { notificationsReducer } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||
|
||||
const App = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const registry = getRegistry();
|
||||
registry.register({ notifications: notificationsReducer });
|
||||
document.title = 'Image Builder | Red Hat Insights';
|
||||
insights.chrome.init();
|
||||
insights.chrome.identifyApp('image-builder');
|
||||
const unregister = insights.chrome.on('APP_NAVIGATION', (event) =>
|
||||
navigate(`/${event.navId}`)
|
||||
);
|
||||
return () => {
|
||||
unregister();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NotificationsPortal />
|
||||
<Router childProps={ props } />
|
||||
</React.Fragment>
|
||||
useEffect(() => {
|
||||
const registry = getRegistry();
|
||||
registry.register({ notifications: notificationsReducer });
|
||||
document.title = 'Image Builder | Red Hat Insights';
|
||||
insights.chrome.init();
|
||||
insights.chrome.identifyApp('image-builder');
|
||||
const unregister = insights.chrome.on('APP_NAVIGATION', (event) =>
|
||||
navigate(`/${event.navId}`)
|
||||
);
|
||||
return () => {
|
||||
unregister();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NotificationsPortal />
|
||||
<Router childProps={props} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -7,11 +7,16 @@ import { getBaseName } from '@redhat-cloud-services/frontend-components-utilitie
|
|||
import logger from 'redux-logger';
|
||||
|
||||
const ImageBuilder = () => (
|
||||
<Provider store={ init({}, ...[ process.env.NODE_ENV !== 'production' ? logger : undefined ]).getStore() }>
|
||||
<Router basename={ getBaseName(window.location.pathname) }>
|
||||
<App />
|
||||
</Router>
|
||||
</Provider>
|
||||
<Provider
|
||||
store={init(
|
||||
{},
|
||||
...[process.env.NODE_ENV !== 'production' ? logger : undefined]
|
||||
).getStore()}
|
||||
>
|
||||
<Router basename={getBaseName(window.location.pathname)}>
|
||||
<App />
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
export default ImageBuilder;
|
||||
|
|
|
|||
|
|
@ -12,469 +12,506 @@ import { composeAdded } from '../../store/actions/actions';
|
|||
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||
|
||||
import {
|
||||
review,
|
||||
awsTarget,
|
||||
registration,
|
||||
googleCloudTarger,
|
||||
msAzureTarget,
|
||||
packages,
|
||||
imageOutput,
|
||||
fileSystemConfiguration,
|
||||
imageName
|
||||
review,
|
||||
awsTarget,
|
||||
registration,
|
||||
googleCloudTarger,
|
||||
msAzureTarget,
|
||||
packages,
|
||||
imageOutput,
|
||||
fileSystemConfiguration,
|
||||
imageName,
|
||||
} from './steps';
|
||||
|
||||
import {
|
||||
fileSystemConfigurationValidator,
|
||||
targetEnvironmentValidator,
|
||||
fileSystemConfigurationValidator,
|
||||
targetEnvironmentValidator,
|
||||
} from './validators';
|
||||
|
||||
const handleKeyDown = (e, handleClose) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClose();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = (values) => {
|
||||
let customizations = {
|
||||
packages: values['selected-packages']?.map(p => p.name),
|
||||
let customizations = {
|
||||
packages: values['selected-packages']?.map((p) => p.name),
|
||||
};
|
||||
|
||||
if (values['register-system'] === 'register-now-insights') {
|
||||
customizations.subscription = {
|
||||
'activation-key': values['subscription-activation-key'],
|
||||
insights: true,
|
||||
organization: Number(values['subscription-organization-id']),
|
||||
'server-url': 'subscription.rhsm.redhat.com',
|
||||
'base-url': 'https://cdn.redhat.com/',
|
||||
};
|
||||
} else if (values['register-system'] === 'register-now') {
|
||||
customizations.subscription = {
|
||||
'activation-key': values['subscription-activation-key'],
|
||||
insights: false,
|
||||
organization: Number(values['subscription-organization-id']),
|
||||
'server-url': 'subscription.rhsm.redhat.com',
|
||||
'base-url': 'https://cdn.redhat.com/',
|
||||
};
|
||||
}
|
||||
|
||||
if (values['file-system-config-toggle'] === 'manual') {
|
||||
customizations.filesystem = [];
|
||||
for (let fsc of values['file-system-configuration']) {
|
||||
customizations.filesystem.push({
|
||||
mountpoint: fsc.mountpoint,
|
||||
min_size: fsc.size * fsc.unit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let requests = [];
|
||||
if (values['target-environment']?.aws) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {
|
||||
share_with_accounts: [values['aws-account-id']],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.gcp) {
|
||||
let share = '';
|
||||
switch (values['google-account-type']) {
|
||||
case 'googleAccount':
|
||||
share = `user:${values['google-email']}`;
|
||||
break;
|
||||
case 'serviceAccount':
|
||||
share = `serviceAccount:${values['google-email']}`;
|
||||
break;
|
||||
case 'googleGroup':
|
||||
share = `group:${values['google-email']}`;
|
||||
break;
|
||||
case 'domain':
|
||||
share = `domain:${values['google-domain']}`;
|
||||
break;
|
||||
}
|
||||
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'gcp',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: [share],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
};
|
||||
|
||||
if (values['register-system'] === 'register-now-insights') {
|
||||
customizations.subscription = {
|
||||
'activation-key': values['subscription-activation-key'],
|
||||
insights: true,
|
||||
organization: Number(values['subscription-organization-id']),
|
||||
'server-url': 'subscription.rhsm.redhat.com',
|
||||
'base-url': 'https://cdn.redhat.com/',
|
||||
};
|
||||
} else if (values['register-system'] === 'register-now') {
|
||||
customizations.subscription = {
|
||||
'activation-key': values['subscription-activation-key'],
|
||||
insights: false,
|
||||
organization: Number(values['subscription-organization-id']),
|
||||
'server-url': 'subscription.rhsm.redhat.com',
|
||||
'base-url': 'https://cdn.redhat.com/',
|
||||
};
|
||||
}
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['file-system-config-toggle'] === 'manual') {
|
||||
customizations.filesystem = [];
|
||||
for (let fsc of values['file-system-configuration']) {
|
||||
customizations.filesystem.push({
|
||||
mountpoint: fsc.mountpoint,
|
||||
min_size: fsc.size * fsc.unit,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (values['target-environment']?.azure) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vhd',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {
|
||||
tenant_id: values['azure-tenant-id'],
|
||||
subscription_id: values['azure-subscription-id'],
|
||||
resource_group: values['azure-resource-group'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
let requests = [];
|
||||
if (values['target-environment']?.aws) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {
|
||||
share_with_accounts: [ values['aws-account-id'] ],
|
||||
},
|
||||
},
|
||||
}],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
if (values['target-environment']?.vsphere) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vsphere',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.gcp) {
|
||||
let share = '';
|
||||
switch (values['google-account-type']) {
|
||||
case 'googleAccount':
|
||||
share = `user:${values['google-email']}`;
|
||||
break;
|
||||
case 'serviceAccount':
|
||||
share = `serviceAccount:${values['google-email']}`;
|
||||
break;
|
||||
case 'googleGroup':
|
||||
share = `group:${values['google-email']}`;
|
||||
break;
|
||||
case 'domain':
|
||||
share = `domain:${values['google-domain']}`;
|
||||
break;
|
||||
}
|
||||
if (values['target-environment']?.['guest-image']) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'guest-image',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'gcp',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: [ share ],
|
||||
},
|
||||
},
|
||||
}],
|
||||
customizations,
|
||||
};
|
||||
if (values['target-environment']?.['image-installer']) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'image-installer',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.azure) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vhd',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {
|
||||
tenant_id: values['azure-tenant-id'],
|
||||
subscription_id: values['azure-subscription-id'],
|
||||
resource_group: values['azure-resource-group'],
|
||||
},
|
||||
},
|
||||
}],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.vsphere) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vsphere',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {}
|
||||
}
|
||||
}],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.['guest-image']) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'guest-image',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {}
|
||||
}
|
||||
}],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if (values['target-environment']?.['image-installer']) {
|
||||
let request = {
|
||||
distribution: values.release,
|
||||
image_name: values?.['image-name'],
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'image-installer',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {}
|
||||
}
|
||||
}],
|
||||
customizations,
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
return requests;
|
||||
return requests;
|
||||
};
|
||||
|
||||
const parseSizeUnit = (bytesize) => {
|
||||
let size;
|
||||
let unit;
|
||||
let size;
|
||||
let unit;
|
||||
|
||||
if (bytesize % UNIT_GIB === 0) {
|
||||
size = bytesize / UNIT_GIB;
|
||||
unit = UNIT_GIB;
|
||||
} else if (bytesize % UNIT_MIB === 0) {
|
||||
size = bytesize / UNIT_MIB;
|
||||
unit = UNIT_MIB;
|
||||
} else if (bytesize % UNIT_KIB === 0) {
|
||||
size = bytesize / UNIT_KIB;
|
||||
unit = UNIT_KIB;
|
||||
}
|
||||
if (bytesize % UNIT_GIB === 0) {
|
||||
size = bytesize / UNIT_GIB;
|
||||
unit = UNIT_GIB;
|
||||
} else if (bytesize % UNIT_MIB === 0) {
|
||||
size = bytesize / UNIT_MIB;
|
||||
unit = UNIT_MIB;
|
||||
} else if (bytesize % UNIT_KIB === 0) {
|
||||
size = bytesize / UNIT_KIB;
|
||||
unit = UNIT_KIB;
|
||||
}
|
||||
|
||||
return [ size, unit ];
|
||||
return [size, unit];
|
||||
};
|
||||
|
||||
const getPackageDescription = async (release, arch, packageName) => {
|
||||
const args = [
|
||||
release,
|
||||
arch,
|
||||
packageName
|
||||
];
|
||||
let { data, meta } = await api.getPackages(...args);
|
||||
let summary;
|
||||
const args = [release, arch, packageName];
|
||||
let { data, meta } = await api.getPackages(...args);
|
||||
let summary;
|
||||
|
||||
// the package should be found in the 0 index
|
||||
// if not then fetch all package matches and search for the package
|
||||
if (data[0]?.name === packageName) {
|
||||
summary = data[0]?.summary;
|
||||
} else {
|
||||
if (data?.length !== meta.count) {
|
||||
({ data } = await api.getPackages(...args, meta.count));
|
||||
}
|
||||
|
||||
const pack = data.find(pack => packageName === pack.name);
|
||||
summary = pack?.summary;
|
||||
// the package should be found in the 0 index
|
||||
// if not then fetch all package matches and search for the package
|
||||
if (data[0]?.name === packageName) {
|
||||
summary = data[0]?.summary;
|
||||
} else {
|
||||
if (data?.length !== meta.count) {
|
||||
({ data } = await api.getPackages(...args, meta.count));
|
||||
}
|
||||
|
||||
// if no matching package is found return an empty string for description
|
||||
return summary || '';
|
||||
} ;
|
||||
const pack = data.find((pack) => packageName === pack.name);
|
||||
summary = pack?.summary;
|
||||
}
|
||||
|
||||
// if no matching package is found return an empty string for description
|
||||
return summary || '';
|
||||
};
|
||||
|
||||
// map the compose request object to the expected form state
|
||||
const requestToState = (composeRequest) => {
|
||||
if (composeRequest) {
|
||||
const imageRequest = composeRequest.image_requests[0];
|
||||
const uploadRequest = imageRequest.upload_request;
|
||||
let formState = {};
|
||||
if (composeRequest) {
|
||||
const imageRequest = composeRequest.image_requests[0];
|
||||
const uploadRequest = imageRequest.upload_request;
|
||||
let formState = {};
|
||||
|
||||
formState['image-name'] = composeRequest.image_name;
|
||||
formState['image-name'] = composeRequest.image_name;
|
||||
|
||||
formState.release = composeRequest?.distribution;
|
||||
// set defaults for target environment first
|
||||
formState['target-environment'] = {
|
||||
aws: false,
|
||||
azure: false,
|
||||
gcp: false,
|
||||
'guest-image': false,
|
||||
};
|
||||
// then select the one from the request
|
||||
// if the image type is to a cloud provider we use the upload_request.type
|
||||
// or if the image is intended for download we use the image_type
|
||||
let targetEnvironment;
|
||||
if (uploadRequest.type === 'aws.s3') {
|
||||
targetEnvironment = imageRequest.image_type;
|
||||
} else {
|
||||
targetEnvironment = uploadRequest.type;
|
||||
}
|
||||
|
||||
formState['target-environment'][targetEnvironment] = true;
|
||||
|
||||
if (targetEnvironment === 'aws') {
|
||||
formState['aws-account-id'] = uploadRequest?.options?.share_with_accounts[0];
|
||||
} else if (targetEnvironment === 'azure') {
|
||||
formState['azure-tenant-id'] = uploadRequest?.options?.tenant_id;
|
||||
formState['azure-subscription-id'] = uploadRequest?.options?.subscription_id;
|
||||
formState['azure-resource-group'] = uploadRequest?.options?.resource_group;
|
||||
} else if (targetEnvironment === 'gcp') {
|
||||
// parse google account info
|
||||
// roughly in the format `accountType:accountEmail`
|
||||
const accountInfo = uploadRequest?.options?.share_with_accounts[0];
|
||||
const [ accountTypePrefix, account ] = accountInfo.split(':');
|
||||
|
||||
switch (accountTypePrefix) {
|
||||
case 'user':
|
||||
formState['google-account-type'] = 'googleAccount';
|
||||
formState['google-email'] = account;
|
||||
break;
|
||||
case 'serviceAccount':
|
||||
formState['google-account-type'] = 'serviceAccount';
|
||||
formState['google-email'] = account;
|
||||
break;
|
||||
case 'group':
|
||||
formState['google-account-type'] = 'googleGroup';
|
||||
formState['google-email'] = account;
|
||||
break;
|
||||
case 'domain':
|
||||
formState['google-account-type'] = 'domain';
|
||||
formState['google-domain'] = account;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// customizations
|
||||
// packages
|
||||
let packs = [];
|
||||
composeRequest?.customizations?.packages?.forEach(async (packName) => {
|
||||
const packageDescription = await getPackageDescription(composeRequest?.distribution, imageRequest?.architecture, packName);
|
||||
const pack = ({
|
||||
name: packName,
|
||||
summary: packageDescription
|
||||
});
|
||||
packs.push(pack);
|
||||
});
|
||||
formState['selected-packages'] = packs;
|
||||
|
||||
// filesystem
|
||||
const fs = composeRequest?.customizations?.filesystem;
|
||||
if (fs) {
|
||||
formState['file-system-config-toggle'] = 'manual';
|
||||
let fileSystemConfiguration = [];
|
||||
for (let fsc of fs) {
|
||||
const [ size, unit ] = parseSizeUnit(fsc.min_size);
|
||||
fileSystemConfiguration.push({
|
||||
mountpoint: fsc.mountpoint,
|
||||
size,
|
||||
unit
|
||||
});
|
||||
}
|
||||
|
||||
formState['file-system-configuration'] = fileSystemConfiguration;
|
||||
}
|
||||
|
||||
// subscription
|
||||
const subscription = composeRequest?.customizations?.subscription;
|
||||
if (subscription) {
|
||||
if (subscription.insights) {
|
||||
formState['register-system'] = 'register-now-insights';
|
||||
} else {
|
||||
formState['register-system'] = 'register-now';
|
||||
}
|
||||
|
||||
formState['subscription-activation-key'] = subscription['activation-key'];
|
||||
formState['subscription-organization-id'] = subscription.organization;
|
||||
} else {
|
||||
formState['register-system'] = 'register-later';
|
||||
}
|
||||
|
||||
return formState;
|
||||
formState.release = composeRequest?.distribution;
|
||||
// set defaults for target environment first
|
||||
formState['target-environment'] = {
|
||||
aws: false,
|
||||
azure: false,
|
||||
gcp: false,
|
||||
'guest-image': false,
|
||||
};
|
||||
// then select the one from the request
|
||||
// if the image type is to a cloud provider we use the upload_request.type
|
||||
// or if the image is intended for download we use the image_type
|
||||
let targetEnvironment;
|
||||
if (uploadRequest.type === 'aws.s3') {
|
||||
targetEnvironment = imageRequest.image_type;
|
||||
} else {
|
||||
return;
|
||||
targetEnvironment = uploadRequest.type;
|
||||
}
|
||||
|
||||
formState['target-environment'][targetEnvironment] = true;
|
||||
|
||||
if (targetEnvironment === 'aws') {
|
||||
formState['aws-account-id'] =
|
||||
uploadRequest?.options?.share_with_accounts[0];
|
||||
} else if (targetEnvironment === 'azure') {
|
||||
formState['azure-tenant-id'] = uploadRequest?.options?.tenant_id;
|
||||
formState['azure-subscription-id'] =
|
||||
uploadRequest?.options?.subscription_id;
|
||||
formState['azure-resource-group'] =
|
||||
uploadRequest?.options?.resource_group;
|
||||
} else if (targetEnvironment === 'gcp') {
|
||||
// parse google account info
|
||||
// roughly in the format `accountType:accountEmail`
|
||||
const accountInfo = uploadRequest?.options?.share_with_accounts[0];
|
||||
const [accountTypePrefix, account] = accountInfo.split(':');
|
||||
|
||||
switch (accountTypePrefix) {
|
||||
case 'user':
|
||||
formState['google-account-type'] = 'googleAccount';
|
||||
formState['google-email'] = account;
|
||||
break;
|
||||
case 'serviceAccount':
|
||||
formState['google-account-type'] = 'serviceAccount';
|
||||
formState['google-email'] = account;
|
||||
break;
|
||||
case 'group':
|
||||
formState['google-account-type'] = 'googleGroup';
|
||||
formState['google-email'] = account;
|
||||
break;
|
||||
case 'domain':
|
||||
formState['google-account-type'] = 'domain';
|
||||
formState['google-domain'] = account;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// customizations
|
||||
// packages
|
||||
let packs = [];
|
||||
composeRequest?.customizations?.packages?.forEach(async (packName) => {
|
||||
const packageDescription = await getPackageDescription(
|
||||
composeRequest?.distribution,
|
||||
imageRequest?.architecture,
|
||||
packName
|
||||
);
|
||||
const pack = {
|
||||
name: packName,
|
||||
summary: packageDescription,
|
||||
};
|
||||
packs.push(pack);
|
||||
});
|
||||
formState['selected-packages'] = packs;
|
||||
|
||||
// filesystem
|
||||
const fs = composeRequest?.customizations?.filesystem;
|
||||
if (fs) {
|
||||
formState['file-system-config-toggle'] = 'manual';
|
||||
let fileSystemConfiguration = [];
|
||||
for (let fsc of fs) {
|
||||
const [size, unit] = parseSizeUnit(fsc.min_size);
|
||||
fileSystemConfiguration.push({
|
||||
mountpoint: fsc.mountpoint,
|
||||
size,
|
||||
unit,
|
||||
});
|
||||
}
|
||||
|
||||
formState['file-system-configuration'] = fileSystemConfiguration;
|
||||
}
|
||||
|
||||
// subscription
|
||||
const subscription = composeRequest?.customizations?.subscription;
|
||||
if (subscription) {
|
||||
if (subscription.insights) {
|
||||
formState['register-system'] = 'register-now-insights';
|
||||
} else {
|
||||
formState['register-system'] = 'register-now';
|
||||
}
|
||||
|
||||
formState['subscription-activation-key'] = subscription['activation-key'];
|
||||
formState['subscription-organization-id'] = subscription.organization;
|
||||
} else {
|
||||
formState['register-system'] = 'register-later';
|
||||
}
|
||||
|
||||
return formState;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const formStepHistory = (composeRequest) => {
|
||||
if (composeRequest) {
|
||||
const imageRequest = composeRequest.image_requests[0];
|
||||
const uploadRequest = imageRequest.upload_request;
|
||||
let steps = [
|
||||
'image-output'
|
||||
];
|
||||
if (composeRequest) {
|
||||
const imageRequest = composeRequest.image_requests[0];
|
||||
const uploadRequest = imageRequest.upload_request;
|
||||
let steps = ['image-output'];
|
||||
|
||||
if (uploadRequest.type === 'aws') {
|
||||
steps.push('aws-target-env');
|
||||
} else if (uploadRequest.type === 'azure') {
|
||||
steps.push('azure-target-env');
|
||||
} else if (uploadRequest.type === 'gcp') {
|
||||
steps.push('google-cloud-target-env');
|
||||
}
|
||||
|
||||
if (isRhel(composeRequest?.distribution)) {
|
||||
steps.push('registration');
|
||||
}
|
||||
|
||||
steps = steps.concat([
|
||||
'File system configuration',
|
||||
'packages',
|
||||
'image-name'
|
||||
]);
|
||||
|
||||
return steps;
|
||||
} else {
|
||||
return [];
|
||||
if (uploadRequest.type === 'aws') {
|
||||
steps.push('aws-target-env');
|
||||
} else if (uploadRequest.type === 'azure') {
|
||||
steps.push('azure-target-env');
|
||||
} else if (uploadRequest.type === 'gcp') {
|
||||
steps.push('google-cloud-target-env');
|
||||
}
|
||||
|
||||
if (isRhel(composeRequest?.distribution)) {
|
||||
steps.push('registration');
|
||||
}
|
||||
|
||||
steps = steps.concat([
|
||||
'File system configuration',
|
||||
'packages',
|
||||
'image-name',
|
||||
]);
|
||||
|
||||
return steps;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const CreateImageWizard = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const composeRequest = location?.state?.composeRequest;
|
||||
const initialState = requestToState(composeRequest);
|
||||
const stepHistory = formStepHistory(composeRequest);
|
||||
const composeRequest = location?.state?.composeRequest;
|
||||
const initialState = requestToState(composeRequest);
|
||||
const stepHistory = formStepHistory(composeRequest);
|
||||
|
||||
const handleClose = () => navigate('/');
|
||||
const handleClose = () => navigate('/');
|
||||
|
||||
return <ImageCreator
|
||||
onClose={ handleClose }
|
||||
onSubmit={ ({ values, setIsSaving }) => {
|
||||
setIsSaving(() => true);
|
||||
const requests = onSave(values);
|
||||
Promise.all(requests.map(request => api.composeImage(request).then((response) => {
|
||||
dispatch(composeAdded({
|
||||
return (
|
||||
<ImageCreator
|
||||
onClose={handleClose}
|
||||
onSubmit={({ values, setIsSaving }) => {
|
||||
setIsSaving(() => true);
|
||||
const requests = onSave(values);
|
||||
Promise.all(
|
||||
requests.map((request) =>
|
||||
api.composeImage(request).then((response) => {
|
||||
dispatch(
|
||||
composeAdded(
|
||||
{
|
||||
...response,
|
||||
request,
|
||||
image_status: { status: 'pending' }
|
||||
}, true));
|
||||
})))
|
||||
.then(() => {
|
||||
navigate('/');
|
||||
dispatch(addNotification({
|
||||
variant: 'success',
|
||||
title: 'Your image is being created',
|
||||
}));
|
||||
image_status: { status: 'pending' },
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
navigate('/');
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'success',
|
||||
title: 'Your image is being created',
|
||||
})
|
||||
);
|
||||
|
||||
setIsSaving(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch(addNotification({
|
||||
variant: 'danger',
|
||||
title: 'Your image could not be created',
|
||||
description: 'Status code ' + err.response.status + ': ' + err.response.statusText,
|
||||
}));
|
||||
setIsSaving(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'danger',
|
||||
title: 'Your image could not be created',
|
||||
description:
|
||||
'Status code ' +
|
||||
err.response.status +
|
||||
': ' +
|
||||
err.response.statusText,
|
||||
})
|
||||
);
|
||||
|
||||
setIsSaving(false);
|
||||
});
|
||||
} }
|
||||
defaultArch="x86_64"
|
||||
customValidatorMapper={ { fileSystemConfigurationValidator, targetEnvironmentValidator } }
|
||||
schema={ {
|
||||
setIsSaving(false);
|
||||
});
|
||||
}}
|
||||
defaultArch="x86_64"
|
||||
customValidatorMapper={{
|
||||
fileSystemConfigurationValidator,
|
||||
targetEnvironmentValidator,
|
||||
}}
|
||||
schema={{
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.WIZARD,
|
||||
name: 'image-builder-wizard',
|
||||
className: 'image_builder',
|
||||
isDynamic: true,
|
||||
inModal: true,
|
||||
onKeyDown: (e) => {
|
||||
handleKeyDown(e, handleClose);
|
||||
},
|
||||
buttonLabels: {
|
||||
submit: 'Create image',
|
||||
},
|
||||
showTitles: true,
|
||||
title: 'Create image',
|
||||
crossroads: ['target-environment', 'release'],
|
||||
description: (
|
||||
<>
|
||||
Image builder allows you to create a custom image and push it to
|
||||
target environments. <DocumentationButton />
|
||||
</>
|
||||
),
|
||||
// order in this array does not reflect order in wizard nav, this order is managed inside
|
||||
// of each step by `nextStep` property!
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.WIZARD,
|
||||
name: 'image-builder-wizard',
|
||||
className: 'image_builder',
|
||||
isDynamic: true,
|
||||
inModal: true,
|
||||
onKeyDown: (e) => { handleKeyDown(e, handleClose); },
|
||||
buttonLabels: {
|
||||
submit: 'Create image',
|
||||
},
|
||||
showTitles: true,
|
||||
title: 'Create image',
|
||||
crossroads: [ 'target-environment', 'release' ],
|
||||
description: <>Image builder allows you to create a custom image and push it to target environments. <DocumentationButton /></>,
|
||||
// order in this array does not reflect order in wizard nav, this order is managed inside
|
||||
// of each step by `nextStep` property!
|
||||
fields: [
|
||||
imageOutput,
|
||||
awsTarget,
|
||||
googleCloudTarger,
|
||||
msAzureTarget,
|
||||
registration,
|
||||
packages,
|
||||
fileSystemConfiguration,
|
||||
imageName,
|
||||
review,
|
||||
],
|
||||
initialState: {
|
||||
activeStep: location?.state?.initialStep || 'image-output', // name of the active step
|
||||
activeStepIndex: stepHistory.length, // active index
|
||||
maxStepIndex: stepHistory.length, // max achieved index
|
||||
prevSteps: stepHistory, // array with names of previously visited steps
|
||||
}
|
||||
}
|
||||
]
|
||||
} }
|
||||
initialValues={ initialState } />;
|
||||
imageOutput,
|
||||
awsTarget,
|
||||
googleCloudTarger,
|
||||
msAzureTarget,
|
||||
registration,
|
||||
packages,
|
||||
fileSystemConfiguration,
|
||||
imageName,
|
||||
review,
|
||||
],
|
||||
initialState: {
|
||||
activeStep: location?.state?.initialStep || 'image-output', // name of the active step
|
||||
activeStepIndex: stepHistory.length, // active index
|
||||
maxStepIndex: stepHistory.length, // max achieved index
|
||||
prevSteps: stepHistory, // array with names of previously visited steps
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
initialValues={initialState}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateImageWizard;
|
||||
|
|
|
|||
|
|
@ -16,52 +16,71 @@ import FileSystemConfiguration from './formComponents/FileSystemConfiguration';
|
|||
import FileSystemConfigToggle from './formComponents/FileSystemConfigToggle';
|
||||
import ImageOutputReleaseSelect from './formComponents/ImageOutputReleaseSelect';
|
||||
|
||||
const ImageCreator = ({ schema, onSubmit, onClose, customComponentMapper, customValidatorMapper, defaultArch, className, ...props }) => {
|
||||
return schema ? <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: Packages,
|
||||
defaultArch
|
||||
},
|
||||
'radio-popover': RadioWithPopover,
|
||||
'azure-auth-expandable': AzureAuthExpandable,
|
||||
'azure-auth-button': AzureAuthButton,
|
||||
'activation-keys': ActivationKeys,
|
||||
'file-system-config-toggle': FileSystemConfigToggle,
|
||||
'file-system-configuration': FileSystemConfiguration,
|
||||
'image-output-release-select': ImageOutputReleaseSelect,
|
||||
...customComponentMapper,
|
||||
} }
|
||||
onCancel={ onClose }
|
||||
{ ...props } /> : <Spinner />;
|
||||
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: Packages,
|
||||
defaultArch,
|
||||
},
|
||||
'radio-popover': RadioWithPopover,
|
||||
'azure-auth-expandable': AzureAuthExpandable,
|
||||
'azure-auth-button': AzureAuthButton,
|
||||
'activation-keys': ActivationKeys,
|
||||
'file-system-config-toggle': FileSystemConfigToggle,
|
||||
'file-system-configuration': FileSystemConfiguration,
|
||||
'image-output-release-select': ImageOutputReleaseSelect,
|
||||
...customComponentMapper,
|
||||
}}
|
||||
onCancel={onClose}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<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
|
||||
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,71 +1,86 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup, Spinner, Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
||||
import {
|
||||
FormGroup,
|
||||
Spinner,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import api from '../../../api';
|
||||
|
||||
const ActivationKeys = ({ label, isRequired, ...props }) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [ activationKeys, setActivationKeys ] = useState([]);
|
||||
const [ isOpen, setIsOpen ] = useState(false);
|
||||
const [ isLoading, setIsLoading ] = useState(false);
|
||||
const [ activationKeySelected, selectActivationKey ] = useState(getState()?.values?.['subscription-activation-key']);
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [activationKeys, setActivationKeys] = useState([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activationKeySelected, selectActivationKey] = useState(
|
||||
getState()?.values?.['subscription-activation-key']
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
const data = api.getActivationKeys();
|
||||
data.then(keys => {
|
||||
setActivationKeys(keys);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
const data = api.getActivationKeys();
|
||||
data.then((keys) => {
|
||||
setActivationKeys(keys);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setActivationKey = (_, selection) => {
|
||||
selectActivationKey(selection);
|
||||
setIsOpen(false);
|
||||
change(input.name, selection);
|
||||
};
|
||||
const setActivationKey = (_, selection) => {
|
||||
selectActivationKey(selection);
|
||||
setIsOpen(false);
|
||||
change(input.name, selection);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
selectActivationKey();
|
||||
change(input.name, undefined);
|
||||
};
|
||||
const handleClear = () => {
|
||||
selectActivationKey();
|
||||
change(input.name, undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup isRequired={ isRequired } label={ label } data-testid='subscription-activation-key'>
|
||||
<Select
|
||||
variant={ SelectVariant.typeahead }
|
||||
onToggle={ () => setIsOpen(!isOpen) }
|
||||
onSelect={ setActivationKey }
|
||||
onClear={ handleClear }
|
||||
selections={ activationKeySelected }
|
||||
isOpen={ isOpen }
|
||||
placeholderText="Select activation key"
|
||||
typeAheadAriaLabel="Select activation key">
|
||||
{isLoading &&
|
||||
<SelectOption isNoResultsOption={ true } data-testid='activation-keys-loading'>
|
||||
<Spinner isSVG size="lg" />
|
||||
</SelectOption>
|
||||
}
|
||||
{activationKeys.map((key, index) => (
|
||||
<SelectOption
|
||||
key={ index }
|
||||
value={ key.name } />
|
||||
))}
|
||||
</Select>
|
||||
</FormGroup>);
|
||||
return (
|
||||
<FormGroup
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
data-testid="subscription-activation-key"
|
||||
>
|
||||
<Select
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={() => setIsOpen(!isOpen)}
|
||||
onSelect={setActivationKey}
|
||||
onClear={handleClear}
|
||||
selections={activationKeySelected}
|
||||
isOpen={isOpen}
|
||||
placeholderText="Select activation key"
|
||||
typeAheadAriaLabel="Select activation key"
|
||||
>
|
||||
{isLoading && (
|
||||
<SelectOption
|
||||
isNoResultsOption={true}
|
||||
data-testid="activation-keys-loading"
|
||||
>
|
||||
<Spinner isSVG size="lg" />
|
||||
</SelectOption>
|
||||
)}
|
||||
{activationKeys.map((key, index) => (
|
||||
<SelectOption key={index} value={key.name} />
|
||||
))}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
ActivationKeys.propTypes = {
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
||||
ActivationKeys.defaultProps = {
|
||||
label: '',
|
||||
isRequired: false
|
||||
label: '',
|
||||
isRequired: false,
|
||||
};
|
||||
|
||||
export default ActivationKeys;
|
||||
|
|
|
|||
|
|
@ -3,24 +3,32 @@ import { Button, FormGroup } from '@patternfly/react-core';
|
|||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
|
||||
const AzureAuthButton = () => {
|
||||
const { getState } = useFormApi();
|
||||
const { getState } = useFormApi();
|
||||
|
||||
const tenantId = getState()?.values?.['azure-tenant-id'];
|
||||
const guidRegex = new RegExp('^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', 'i');
|
||||
const tenantId = getState()?.values?.['azure-tenant-id'];
|
||||
const guidRegex = new RegExp(
|
||||
'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
|
||||
'i'
|
||||
);
|
||||
|
||||
return (
|
||||
<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>);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -3,34 +3,41 @@ import { Button, ExpandableSection, Text, Title } from '@patternfly/react-core';
|
|||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
const AzureAuthExpandable = () => {
|
||||
const [ expanded, setExpanded ] = useState(true);
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExpandableSection
|
||||
className='azureAuthExpandable'
|
||||
toggleText={ <Title headingLevel="h3">Authorizing an Azure account</Title> }
|
||||
onToggle={ () => setExpanded(!expanded) }
|
||||
isExpanded={ expanded }>
|
||||
<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" to at least one resource group.<br />
|
||||
</Text>
|
||||
<small>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={ <ExternalLinkAltIcon /> }
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow">
|
||||
Learn more about OAuth 2.0
|
||||
</Button>
|
||||
</small>
|
||||
</ExpandableSection>
|
||||
</>);
|
||||
return (
|
||||
<>
|
||||
<ExpandableSection
|
||||
className="azureAuthExpandable"
|
||||
toggleText={
|
||||
<Title headingLevel="h3">Authorizing an Azure account</Title>
|
||||
}
|
||||
onToggle={() => setExpanded(!expanded)}
|
||||
isExpanded={expanded}
|
||||
>
|
||||
<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" to at least one resource group.
|
||||
<br />
|
||||
</Text>
|
||||
<small>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow"
|
||||
>
|
||||
Learn more about OAuth 2.0
|
||||
</Button>
|
||||
</small>
|
||||
</ExpandableSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AzureAuthExpandable;
|
||||
|
|
|
|||
|
|
@ -4,45 +4,62 @@ import { FormSpy } from '@data-driven-forms/react-form-renderer';
|
|||
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const CustomButtons = ({ buttonLabels: { cancel, submit, back }}) => {
|
||||
const [ isSaving, setIsSaving ] = useState(false);
|
||||
const { handlePrev, formOptions } = useContext(WizardContext);
|
||||
return <FormSpy>
|
||||
{() => (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="button"
|
||||
isDisabled={ !formOptions.valid || formOptions.getState().validating || isSaving }
|
||||
isLoading={ isSaving }
|
||||
onClick={ () => {
|
||||
formOptions.onSubmit({
|
||||
values: formOptions.getState().values,
|
||||
setIsSaving
|
||||
});
|
||||
} }>
|
||||
{ isSaving ? 'Creating image' : submit}
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={ handlePrev } isDisabled={ isSaving }>
|
||||
{back}
|
||||
</Button>
|
||||
<div className="pf-c-wizard__footer-cancel">
|
||||
<Button type="button" variant="link" onClick={ formOptions.onCancel } isDisabled={ isSaving }>
|
||||
{cancel}
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</FormSpy>;
|
||||
const CustomButtons = ({ buttonLabels: { cancel, submit, back } }) => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const { handlePrev, formOptions } = useContext(WizardContext);
|
||||
return (
|
||||
<FormSpy>
|
||||
{() => (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="button"
|
||||
isDisabled={
|
||||
!formOptions.valid ||
|
||||
formOptions.getState().validating ||
|
||||
isSaving
|
||||
}
|
||||
isLoading={isSaving}
|
||||
onClick={() => {
|
||||
formOptions.onSubmit({
|
||||
values: formOptions.getState().values,
|
||||
setIsSaving,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isSaving ? 'Creating image' : submit}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handlePrev}
|
||||
isDisabled={isSaving}
|
||||
>
|
||||
{back}
|
||||
</Button>
|
||||
<div className="pf-c-wizard__footer-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,
|
||||
}),
|
||||
isSaving: PropTypes.bool
|
||||
buttonLabels: PropTypes.shape({
|
||||
cancel: PropTypes.node,
|
||||
submit: PropTypes.node,
|
||||
back: PropTypes.node,
|
||||
}),
|
||||
isSaving: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CustomButtons;
|
||||
|
|
|
|||
|
|
@ -1,48 +1,46 @@
|
|||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from '@patternfly/react-core';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core';
|
||||
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
|
||||
const FileSystemConfigToggle = ({ ...props }) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [ selected, setSelected ] =
|
||||
useState(getState()?.values?.['file-system-config-toggle'] || 'auto');
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [selected, setSelected] = useState(
|
||||
getState()?.values?.['file-system-config-toggle'] || 'auto'
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
change(input.name, selected);
|
||||
}, [ selected ]);
|
||||
useEffect(() => {
|
||||
change(input.name, selected);
|
||||
}, [selected]);
|
||||
|
||||
const onClick = (_, evt) => {
|
||||
setSelected(evt.currentTarget.id);
|
||||
};
|
||||
const onClick = (_, evt) => {
|
||||
setSelected(evt.currentTarget.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToggleGroup
|
||||
data-testid="fsc-paritioning-toggle"
|
||||
aria-label="Automatic partitioning toggle">
|
||||
<ToggleGroupItem
|
||||
onChange={ onClick }
|
||||
text="Use automatic partitioning"
|
||||
buttonId="auto"
|
||||
isSelected={ selected === 'auto' } />
|
||||
<ToggleGroupItem
|
||||
onChange={ onClick }
|
||||
text="Manually configure partitions"
|
||||
buttonId="manual"
|
||||
isSelected={ selected === 'manual' }
|
||||
data-testid="file-system-config-toggle-manual" />
|
||||
</ToggleGroup>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ToggleGroup
|
||||
data-testid="fsc-paritioning-toggle"
|
||||
aria-label="Automatic partitioning toggle"
|
||||
>
|
||||
<ToggleGroupItem
|
||||
onChange={onClick}
|
||||
text="Use automatic partitioning"
|
||||
buttonId="auto"
|
||||
isSelected={selected === 'auto'}
|
||||
/>
|
||||
<ToggleGroupItem
|
||||
onChange={onClick}
|
||||
text="Manually configure partitions"
|
||||
buttonId="manual"
|
||||
isSelected={selected === 'manual'}
|
||||
data-testid="file-system-config-toggle-manual"
|
||||
/>
|
||||
</ToggleGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemConfigToggle;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { HelpIcon, MinusCircleIcon, PlusCircleIcon } from '@patternfly/react-icons';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Popover,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
HelpIcon,
|
||||
MinusCircleIcon,
|
||||
PlusCircleIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Popover,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
TableComposable,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableComposable,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
} from '@patternfly/react-table';
|
||||
import styles from '@patternfly/react-styles/css/components/Table/table';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
|
|
@ -30,298 +30,352 @@ import MountPoint from './MountPoint';
|
|||
import SizeUnit from './SizeUnit';
|
||||
|
||||
let initialRow = {
|
||||
id: uuidv4(),
|
||||
mountpoint: '/',
|
||||
fstype: 'xfs',
|
||||
size: 10,
|
||||
unit: UNIT_GIB,
|
||||
id: uuidv4(),
|
||||
mountpoint: '/',
|
||||
fstype: 'xfs',
|
||||
size: 10,
|
||||
unit: UNIT_GIB,
|
||||
};
|
||||
|
||||
const FileSystemConfiguration = ({ ...props }) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [ draggedItemId, setDraggedItemId ] = useState(null);
|
||||
const [ draggingToItemIndex, setDraggingToItemIndex ] = useState(null);
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
const [ itemOrder, setItemOrder ] = useState([ initialRow.id ]);
|
||||
const [ tempItemOrder, setTempItemOrder ] = useState([]);
|
||||
const bodyref = useRef();
|
||||
const [ rows, setRows ] = useState([ initialRow ]);
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [draggedItemId, setDraggedItemId] = useState(null);
|
||||
const [draggingToItemIndex, setDraggingToItemIndex] = useState(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [itemOrder, setItemOrder] = useState([initialRow.id]);
|
||||
const [tempItemOrder, setTempItemOrder] = useState([]);
|
||||
const bodyref = useRef();
|
||||
const [rows, setRows] = useState([initialRow]);
|
||||
|
||||
useEffect(() => {
|
||||
const fsc = getState()?.values?.['file-system-configuration'];
|
||||
if (!fsc) {
|
||||
return;
|
||||
useEffect(() => {
|
||||
const fsc = getState()?.values?.['file-system-configuration'];
|
||||
if (!fsc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRows = [];
|
||||
const newOrder = [];
|
||||
fsc.map((r) => {
|
||||
const id = uuidv4();
|
||||
newRows.push({
|
||||
id,
|
||||
mountpoint: r.mountpoint,
|
||||
fstype: 'xfs',
|
||||
size: r.size,
|
||||
unit: r.unit,
|
||||
});
|
||||
newOrder.push(id);
|
||||
});
|
||||
setRows(newRows);
|
||||
setItemOrder(newOrder);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
change(
|
||||
input.name,
|
||||
itemOrder.map((r) => {
|
||||
for (const r2 of rows) {
|
||||
if (r2.id === r) {
|
||||
return {
|
||||
mountpoint: r2.mountpoint,
|
||||
size: r2.size,
|
||||
unit: r2.unit,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const newRows = [];
|
||||
const newOrder = [];
|
||||
fsc.map(r => {
|
||||
const id = uuidv4();
|
||||
newRows.push({
|
||||
id,
|
||||
mountpoint: r.mountpoint,
|
||||
fstype: 'xfs',
|
||||
size: r.size,
|
||||
unit: r.unit,
|
||||
});
|
||||
newOrder.push(id);
|
||||
});
|
||||
setRows(newRows);
|
||||
setItemOrder(newOrder);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
change(input.name, itemOrder.map(r => {
|
||||
for (const r2 of rows) {
|
||||
if (r2.id === r) {
|
||||
return {
|
||||
mountpoint: r2.mountpoint,
|
||||
size: r2.size,
|
||||
unit: r2.unit,
|
||||
};
|
||||
}
|
||||
}
|
||||
}));
|
||||
}, [ rows, itemOrder ]);
|
||||
|
||||
const addRow = () => {
|
||||
const id = uuidv4();
|
||||
setRows(rows.concat([{
|
||||
id,
|
||||
mountpoint: '/home',
|
||||
fstype: 'xfs',
|
||||
size: 1,
|
||||
unit: UNIT_GIB,
|
||||
}]));
|
||||
setItemOrder(itemOrder.concat([ id ]));
|
||||
};
|
||||
|
||||
const removeRow = id => {
|
||||
let removeIndex = rows.map(e => e.id).indexOf(id);
|
||||
let newRows = [ ...rows ];
|
||||
newRows.splice(removeIndex, 1);
|
||||
|
||||
let removeOrderIndex = itemOrder.indexOf(id);
|
||||
let newOrder = [ ...itemOrder ];
|
||||
newOrder.splice(removeOrderIndex, 1);
|
||||
|
||||
setRows(newRows);
|
||||
setItemOrder(newOrder);
|
||||
};
|
||||
|
||||
const moveItem = (arr, i1, toIndex) => {
|
||||
const fromIndex = arr.indexOf(i1);
|
||||
if (fromIndex === toIndex) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
const temp = arr.splice(fromIndex, 1);
|
||||
arr.splice(toIndex, 0, temp[0]);
|
||||
return arr;
|
||||
};
|
||||
|
||||
const move = itemOrder => {
|
||||
const ulNode = bodyref.current;
|
||||
const nodes = Array.from(ulNode.children);
|
||||
if (nodes.map(node => node.id).every((id, i) => id === itemOrder[i])) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (ulNode.firstChild) {
|
||||
ulNode.removeChild(ulNode.lastChild);
|
||||
}
|
||||
|
||||
itemOrder.forEach(id => {
|
||||
ulNode.appendChild(nodes.find(n => n.id === id));
|
||||
});
|
||||
};
|
||||
|
||||
const onDragOver = evt => {
|
||||
evt.preventDefault();
|
||||
|
||||
const curListItem = evt.target.closest('tr');
|
||||
if (!curListItem || !bodyref.current.contains(curListItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dragId = curListItem.id;
|
||||
const newDraggingToItemIndex = Array.from(bodyref.current.children).findIndex(item => item.id === dragId);
|
||||
if (newDraggingToItemIndex !== draggingToItemIndex) {
|
||||
const tempItemOrder = moveItem([ ...itemOrder ], draggedItemId, newDraggingToItemIndex);
|
||||
move(tempItemOrder);
|
||||
setDraggingToItemIndex(newDraggingToItemIndex);
|
||||
setTempItemOrder(tempItemOrder);
|
||||
}
|
||||
};
|
||||
|
||||
const isValidDrop = evt => {
|
||||
const ulRect = bodyref.current.getBoundingClientRect();
|
||||
return (
|
||||
evt.clientX > ulRect.x &&
|
||||
evt.clientX < ulRect.x + ulRect.width &&
|
||||
evt.clientY > ulRect.y &&
|
||||
evt.clientY < ulRect.y + ulRect.height
|
||||
);
|
||||
};
|
||||
|
||||
const onDragLeave = evt => {
|
||||
if (!isValidDrop(evt)) {
|
||||
move(itemOrder);
|
||||
setDraggingToItemIndex(null);
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = evt => {
|
||||
if (isValidDrop(evt)) {
|
||||
setItemOrder(tempItemOrder);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragStart = evt => {
|
||||
evt.dataTransfer.effectAllowed = 'move';
|
||||
evt.dataTransfer.setData('text/plain', evt.currentTarget.id);
|
||||
evt.currentTarget.classList.add(styles.modifiers.ghostRow);
|
||||
evt.currentTarget.setAttribute('aria-pressed', 'true');
|
||||
setDraggedItemId(evt.currentTarget.id);
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const onDragEnd = evt => {
|
||||
evt.target.classList.remove(styles.modifiers.ghostRow);
|
||||
evt.target.setAttribute('aria-pressed', 'false');
|
||||
setDraggedItemId(null);
|
||||
setDraggingToItemIndex(null);
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const setMountpoint = (id, mp) => {
|
||||
let newRows = [ ...rows ];
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
if (newRows[i].id === id) {
|
||||
let newRow = { ...newRows[i] };
|
||||
newRow.mountpoint = mp;
|
||||
newRows.splice(i, 1, newRow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setRows(newRows);
|
||||
};
|
||||
|
||||
const setSize = (id, s, u) => {
|
||||
let newRows = [ ...rows ];
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
if (newRows[i].id === id) {
|
||||
let newRow = { ...newRows[i] };
|
||||
newRow.size = s;
|
||||
newRow.unit = u;
|
||||
newRows.splice(i, 1, newRow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setRows(newRows);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextContent>
|
||||
<Text component={ TextVariants.h3 }>Configure partitions</Text>
|
||||
</TextContent>
|
||||
{ rows.length > 1 && getState()?.errors?.['file-system-configuration']?.duplicates &&
|
||||
<Alert variant="danger" isInline
|
||||
title="Duplicate mount points: All mount points must be unique. Remove the duplicate or choose a new mount point." />
|
||||
}
|
||||
{ rows.length >= 1 && getState()?.errors?.['file-system-configuration']?.root === false &&
|
||||
<Alert variant="danger" isInline
|
||||
title="No root partition configured." />
|
||||
}
|
||||
<TextContent>
|
||||
<Text>
|
||||
Partitions have been generated and given default values based on best practices from Red Hat,
|
||||
and your selections in previous steps of the wizard.
|
||||
</Text>
|
||||
</TextContent>
|
||||
<TableComposable aria-label="File system table" className={ isDragging && styles.modifiers.dragOver } variant="compact">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<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 />
|
||||
</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 &&
|
||||
getState().errors['file-system-configuration']?.duplicates.indexOf(row.mountpoint) !== -1 &&
|
||||
<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) } />
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
<TextContent>
|
||||
<Button
|
||||
data-testid="file-system-add-partition"
|
||||
className="pf-u-text-align-left"
|
||||
variant="link"
|
||||
icon={ <PlusCircleIcon /> }
|
||||
onClick={ addRow }>
|
||||
Add partition
|
||||
</Button>
|
||||
</TextContent>
|
||||
</>
|
||||
})
|
||||
);
|
||||
}, [rows, itemOrder]);
|
||||
|
||||
const addRow = () => {
|
||||
const id = uuidv4();
|
||||
setRows(
|
||||
rows.concat([
|
||||
{
|
||||
id,
|
||||
mountpoint: '/home',
|
||||
fstype: 'xfs',
|
||||
size: 1,
|
||||
unit: UNIT_GIB,
|
||||
},
|
||||
])
|
||||
);
|
||||
setItemOrder(itemOrder.concat([id]));
|
||||
};
|
||||
|
||||
const removeRow = (id) => {
|
||||
let removeIndex = rows.map((e) => e.id).indexOf(id);
|
||||
let newRows = [...rows];
|
||||
newRows.splice(removeIndex, 1);
|
||||
|
||||
let removeOrderIndex = itemOrder.indexOf(id);
|
||||
let newOrder = [...itemOrder];
|
||||
newOrder.splice(removeOrderIndex, 1);
|
||||
|
||||
setRows(newRows);
|
||||
setItemOrder(newOrder);
|
||||
};
|
||||
|
||||
const moveItem = (arr, i1, toIndex) => {
|
||||
const fromIndex = arr.indexOf(i1);
|
||||
if (fromIndex === toIndex) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
const temp = arr.splice(fromIndex, 1);
|
||||
arr.splice(toIndex, 0, temp[0]);
|
||||
return arr;
|
||||
};
|
||||
|
||||
const move = (itemOrder) => {
|
||||
const ulNode = bodyref.current;
|
||||
const nodes = Array.from(ulNode.children);
|
||||
if (nodes.map((node) => node.id).every((id, i) => id === itemOrder[i])) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (ulNode.firstChild) {
|
||||
ulNode.removeChild(ulNode.lastChild);
|
||||
}
|
||||
|
||||
itemOrder.forEach((id) => {
|
||||
ulNode.appendChild(nodes.find((n) => n.id === id));
|
||||
});
|
||||
};
|
||||
|
||||
const onDragOver = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const curListItem = evt.target.closest('tr');
|
||||
if (!curListItem || !bodyref.current.contains(curListItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dragId = curListItem.id;
|
||||
const newDraggingToItemIndex = Array.from(
|
||||
bodyref.current.children
|
||||
).findIndex((item) => item.id === dragId);
|
||||
if (newDraggingToItemIndex !== draggingToItemIndex) {
|
||||
const tempItemOrder = moveItem(
|
||||
[...itemOrder],
|
||||
draggedItemId,
|
||||
newDraggingToItemIndex
|
||||
);
|
||||
move(tempItemOrder);
|
||||
setDraggingToItemIndex(newDraggingToItemIndex);
|
||||
setTempItemOrder(tempItemOrder);
|
||||
}
|
||||
};
|
||||
|
||||
const isValidDrop = (evt) => {
|
||||
const ulRect = bodyref.current.getBoundingClientRect();
|
||||
return (
|
||||
evt.clientX > ulRect.x &&
|
||||
evt.clientX < ulRect.x + ulRect.width &&
|
||||
evt.clientY > ulRect.y &&
|
||||
evt.clientY < ulRect.y + ulRect.height
|
||||
);
|
||||
};
|
||||
|
||||
const onDragLeave = (evt) => {
|
||||
if (!isValidDrop(evt)) {
|
||||
move(itemOrder);
|
||||
setDraggingToItemIndex(null);
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = (evt) => {
|
||||
if (isValidDrop(evt)) {
|
||||
setItemOrder(tempItemOrder);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragStart = (evt) => {
|
||||
evt.dataTransfer.effectAllowed = 'move';
|
||||
evt.dataTransfer.setData('text/plain', evt.currentTarget.id);
|
||||
evt.currentTarget.classList.add(styles.modifiers.ghostRow);
|
||||
evt.currentTarget.setAttribute('aria-pressed', 'true');
|
||||
setDraggedItemId(evt.currentTarget.id);
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const onDragEnd = (evt) => {
|
||||
evt.target.classList.remove(styles.modifiers.ghostRow);
|
||||
evt.target.setAttribute('aria-pressed', 'false');
|
||||
setDraggedItemId(null);
|
||||
setDraggingToItemIndex(null);
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const setMountpoint = (id, mp) => {
|
||||
let newRows = [...rows];
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
if (newRows[i].id === id) {
|
||||
let newRow = { ...newRows[i] };
|
||||
newRow.mountpoint = mp;
|
||||
newRows.splice(i, 1, newRow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setRows(newRows);
|
||||
};
|
||||
|
||||
const setSize = (id, s, u) => {
|
||||
let newRows = [...rows];
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
if (newRows[i].id === id) {
|
||||
let newRow = { ...newRows[i] };
|
||||
newRow.size = s;
|
||||
newRow.unit = u;
|
||||
newRows.splice(i, 1, newRow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setRows(newRows);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Configure partitions</Text>
|
||||
</TextContent>
|
||||
{rows.length > 1 &&
|
||||
getState()?.errors?.['file-system-configuration']?.duplicates && (
|
||||
<Alert
|
||||
variant="danger"
|
||||
isInline
|
||||
title="Duplicate mount points: All mount points must be unique. Remove the duplicate or choose a new mount point."
|
||||
/>
|
||||
)}
|
||||
{rows.length >= 1 &&
|
||||
getState()?.errors?.['file-system-configuration']?.root === false && (
|
||||
<Alert
|
||||
variant="danger"
|
||||
isInline
|
||||
title="No root partition configured."
|
||||
/>
|
||||
)}
|
||||
<TextContent>
|
||||
<Text>
|
||||
Partitions have been generated and given default values based on best
|
||||
practices from Red Hat, and your selections in previous steps of the
|
||||
wizard.
|
||||
</Text>
|
||||
</TextContent>
|
||||
<TableComposable
|
||||
aria-label="File system table"
|
||||
className={isDragging && styles.modifiers.dragOver}
|
||||
variant="compact"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<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 />
|
||||
</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 &&
|
||||
getState().errors[
|
||||
'file-system-configuration'
|
||||
]?.duplicates.indexOf(row.mountpoint) !== -1 && (
|
||||
<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)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
<TextContent>
|
||||
<Button
|
||||
data-testid="file-system-add-partition"
|
||||
className="pf-u-text-align-left"
|
||||
variant="link"
|
||||
icon={<PlusCircleIcon />}
|
||||
onClick={addRow}
|
||||
>
|
||||
Add partition
|
||||
</Button>
|
||||
</TextContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemConfiguration;
|
||||
|
|
|
|||
|
|
@ -1,58 +1,64 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup, Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
||||
import {
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import { RELEASES } from '../../../constants';
|
||||
import isRhel from '../../../Utilities/isRhel';
|
||||
|
||||
const ImageOutputReleaseSelect = ({ label, isRequired, ...props }) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [ isOpen, setIsOpen ] = useState(false);
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const setRelease = (_, selection) => {
|
||||
change(input.name, selection);
|
||||
setIsOpen(false);
|
||||
};
|
||||
const setRelease = (_, selection) => {
|
||||
change(input.name, selection);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
change(input.name, null);
|
||||
};
|
||||
const handleClear = () => {
|
||||
change(input.name, null);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup isRequired={ isRequired } label={ label }>
|
||||
<Select
|
||||
variant={ SelectVariant.single }
|
||||
onToggle={ () => setIsOpen(!isOpen) }
|
||||
onSelect={ setRelease }
|
||||
onClear={ handleClear }
|
||||
selections={ RELEASES[getState()?.values?.[input.name]] }
|
||||
isOpen={ isOpen }>
|
||||
{
|
||||
Object.entries(RELEASES)
|
||||
.filter(([ key ]) => {
|
||||
// Only show non-RHEL distros in beta
|
||||
if (insights.chrome.isBeta()) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
<FormGroup isRequired={isRequired} label={label}>
|
||||
<Select
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setIsOpen(!isOpen)}
|
||||
onSelect={setRelease}
|
||||
onClear={handleClear}
|
||||
selections={RELEASES[getState()?.values?.[input.name]]}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{Object.entries(RELEASES)
|
||||
.filter(([key]) => {
|
||||
// Only show non-RHEL distros in beta
|
||||
if (insights.chrome.isBeta()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isRhel(key);
|
||||
})
|
||||
.map(([ key, release ], index) => {
|
||||
return <SelectOption key={ index } value={ key }>
|
||||
{ release }
|
||||
</SelectOption>;
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
);
|
||||
return isRhel(key);
|
||||
})
|
||||
.map(([key, release], index) => {
|
||||
return (
|
||||
<SelectOption key={index} value={key}>
|
||||
{release}
|
||||
</SelectOption>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
ImageOutputReleaseSelect.propTypes = {
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ImageOutputReleaseSelect;
|
||||
|
|
|
|||
|
|
@ -1,86 +1,95 @@
|
|||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
TextInput,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
import path from 'path';
|
||||
|
||||
const MountPoint = ({ ...props }) => {
|
||||
// check '/' last!
|
||||
const validPrefixes = [ '/app', '/data', '/home', '/opt', '/srv', '/tmp', '/usr', '/usr/local', '/var', '/' ];
|
||||
const [ isOpen, setIsOpen ] = useState(false);
|
||||
const [ prefix, setPrefix ] = useState('/');
|
||||
const [ suffix, setSuffix ] = useState('');
|
||||
// check '/' last!
|
||||
const validPrefixes = [
|
||||
'/app',
|
||||
'/data',
|
||||
'/home',
|
||||
'/opt',
|
||||
'/srv',
|
||||
'/tmp',
|
||||
'/usr',
|
||||
'/usr/local',
|
||||
'/var',
|
||||
'/',
|
||||
];
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [prefix, setPrefix] = useState('/');
|
||||
const [suffix, setSuffix] = useState('');
|
||||
|
||||
// split
|
||||
useEffect(() => {
|
||||
for (let p of validPrefixes) {
|
||||
if (props.mountpoint.startsWith(p)) {
|
||||
setPrefix(p);
|
||||
setSuffix(props.mountpoint.substring(p.length));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
// split
|
||||
useEffect(() => {
|
||||
for (let p of validPrefixes) {
|
||||
if (props.mountpoint.startsWith(p)) {
|
||||
setPrefix(p);
|
||||
setSuffix(props.mountpoint.substring(p.length));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let suf = suffix;
|
||||
let mp = prefix;
|
||||
if (suf) {
|
||||
if (mp !== '/' && suf[0] !== '/') {
|
||||
suf = '/' + suf;
|
||||
}
|
||||
useEffect(() => {
|
||||
let suf = suffix;
|
||||
let mp = prefix;
|
||||
if (suf) {
|
||||
if (mp !== '/' && suf[0] !== '/') {
|
||||
suf = '/' + suf;
|
||||
}
|
||||
|
||||
mp += suf;
|
||||
}
|
||||
mp += suf;
|
||||
}
|
||||
|
||||
props.onChange(path.normalize(mp));
|
||||
}, [ prefix, suffix ]);
|
||||
props.onChange(path.normalize(mp));
|
||||
}, [prefix, suffix]);
|
||||
|
||||
const onToggle = (isOpen) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
const onToggle = (isOpen) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
const onSelect = (event, selection) => {
|
||||
setPrefix(selection);
|
||||
setIsOpen(false);
|
||||
};
|
||||
const onSelect = (event, selection) => {
|
||||
setPrefix(selection);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="pf-u-w-50"
|
||||
isOpen={ isOpen }
|
||||
onToggle={ onToggle }
|
||||
onSelect={ onSelect }
|
||||
selections={ prefix }
|
||||
variant={ SelectVariant.single }>
|
||||
{validPrefixes.map((pfx, index) => {
|
||||
return <SelectOption key={ index } value={ pfx } />;
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
{ prefix !== '/' &&
|
||||
<TextInput
|
||||
className="pf-u-w-50"
|
||||
type="text"
|
||||
value={ suffix }
|
||||
aria-label="Mount point suffix text input"
|
||||
onChange={ v => setSuffix(v) } />
|
||||
}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="pf-u-w-50"
|
||||
isOpen={isOpen}
|
||||
onToggle={onToggle}
|
||||
onSelect={onSelect}
|
||||
selections={prefix}
|
||||
variant={SelectVariant.single}
|
||||
>
|
||||
{validPrefixes.map((pfx, index) => {
|
||||
return <SelectOption key={index} value={pfx} />;
|
||||
})}
|
||||
</Select>
|
||||
{prefix !== '/' && (
|
||||
<TextInput
|
||||
className="pf-u-w-50"
|
||||
type="text"
|
||||
value={suffix}
|
||||
aria-label="Mount point suffix text input"
|
||||
onChange={(v) => setSuffix(v)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MountPoint.propTypes = {
|
||||
mountpoint: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
mountpoint: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default MountPoint;
|
||||
|
|
|
|||
|
|
@ -4,391 +4,459 @@ import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
|||
import api from '../../../api';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
DualListSelector,
|
||||
DualListSelectorPane,
|
||||
DualListSelectorList,
|
||||
DualListSelectorListItem,
|
||||
DualListSelectorControlsWrapper,
|
||||
DualListSelectorControl,
|
||||
SearchInput,
|
||||
TextContent
|
||||
DualListSelector,
|
||||
DualListSelectorPane,
|
||||
DualListSelectorList,
|
||||
DualListSelectorListItem,
|
||||
DualListSelectorControlsWrapper,
|
||||
DualListSelectorControl,
|
||||
SearchInput,
|
||||
TextContent,
|
||||
} from '@patternfly/react-core';
|
||||
import { AngleDoubleLeftIcon, AngleLeftIcon, AngleDoubleRightIcon, AngleRightIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
AngleDoubleLeftIcon,
|
||||
AngleLeftIcon,
|
||||
AngleDoubleRightIcon,
|
||||
AngleRightIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
// the fields isHidden and isSelected should not be included in the package list sent for image creation
|
||||
const removePackagesDisplayFields = (packages) => packages.map((pack) => ({
|
||||
const removePackagesDisplayFields = (packages) =>
|
||||
packages.map((pack) => ({
|
||||
name: pack.name,
|
||||
summary: pack.summary,
|
||||
}));
|
||||
}));
|
||||
|
||||
const Packages = ({ defaultArch, ...props }) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [ packagesSearchName, setPackagesSearchName ] = useState(undefined);
|
||||
const [ filterAvailable, setFilterAvailable ] = useState(undefined);
|
||||
const [ filterChosen, setFilterChosen ] = useState(undefined);
|
||||
const [ packagesAvailable, setPackagesAvailable ] = useState([]);
|
||||
const [ packagesAvailableFound, setPackagesAvailableFound ] = useState(true);
|
||||
const [ packagesChosen, setPackagesChosen ] = useState([]);
|
||||
const [ packagesChosenFound, setPackagesChosenFound ] = useState(true);
|
||||
const [ focus, setFocus ] = useState('');
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
const [packagesSearchName, setPackagesSearchName] = useState(undefined);
|
||||
const [filterAvailable, setFilterAvailable] = useState(undefined);
|
||||
const [filterChosen, setFilterChosen] = useState(undefined);
|
||||
const [packagesAvailable, setPackagesAvailable] = useState([]);
|
||||
const [packagesAvailableFound, setPackagesAvailableFound] = useState(true);
|
||||
const [packagesChosen, setPackagesChosen] = useState([]);
|
||||
const [packagesChosenFound, setPackagesChosenFound] = useState(true);
|
||||
const [focus, setFocus] = useState('');
|
||||
|
||||
// this effect only triggers on mount
|
||||
useEffect(() => {
|
||||
const selectedPackages = getState()?.values?.['selected-packages'];
|
||||
if (selectedPackages) {
|
||||
setPackagesChosen(selectedPackages);
|
||||
}
|
||||
}, []);
|
||||
// this effect only triggers on mount
|
||||
useEffect(() => {
|
||||
const selectedPackages = getState()?.values?.['selected-packages'];
|
||||
if (selectedPackages) {
|
||||
setPackagesChosen(selectedPackages);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const searchResultsComparator = useCallback((searchTerm) => {
|
||||
return (a, b) => {
|
||||
a = a.name.toLowerCase();
|
||||
b = b.name.toLowerCase();
|
||||
const searchResultsComparator = useCallback((searchTerm) => {
|
||||
return (a, b) => {
|
||||
a = a.name.toLowerCase();
|
||||
b = b.name.toLowerCase();
|
||||
|
||||
// check exact match first
|
||||
if (a === searchTerm) {
|
||||
return -1;
|
||||
}
|
||||
// check exact match first
|
||||
if (a === searchTerm) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b === searchTerm) {
|
||||
return 1;
|
||||
}
|
||||
if (b === searchTerm) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// check for packages that start with the search term
|
||||
if (a.startsWith(searchTerm) && !b.startsWith(searchTerm)) {
|
||||
return -1;
|
||||
}
|
||||
// check for packages that start with the search term
|
||||
if (a.startsWith(searchTerm) && !b.startsWith(searchTerm)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.startsWith(searchTerm) && !a.startsWith(searchTerm)) {
|
||||
return 1;
|
||||
}
|
||||
if (b.startsWith(searchTerm) && !a.startsWith(searchTerm)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// if both (or neither) start with the search term
|
||||
// sort alphabetically
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
// if both (or neither) start with the search term
|
||||
// sort alphabetically
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b < a) {
|
||||
return 1;
|
||||
}
|
||||
if (b < a) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
return 0;
|
||||
};
|
||||
});
|
||||
|
||||
const setPackagesAvailableSorted = (
|
||||
packageList,
|
||||
filter = filterAvailable
|
||||
) => {
|
||||
const sortResults = packageList.sort(searchResultsComparator(filter));
|
||||
setPackagesAvailable(sortResults);
|
||||
};
|
||||
|
||||
const setPackagesChosenSorted = (packageList) => {
|
||||
const sortResults = packageList.sort(searchResultsComparator(filterChosen));
|
||||
setPackagesChosen(sortResults);
|
||||
};
|
||||
|
||||
// filter the packages by name
|
||||
const filterPackagesAvailable = (packageList) => {
|
||||
return packageList.filter((availablePackage) => {
|
||||
// returns true if no packages in the available or chosen list have the same name
|
||||
return !packagesChosen.some(
|
||||
(chosenPackage) => availablePackage.name === chosenPackage.name
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getAllPackages = async () => {
|
||||
const args = [
|
||||
getState()?.values?.release,
|
||||
getState()?.values?.architecture || defaultArch,
|
||||
packagesSearchName,
|
||||
];
|
||||
let { data, meta } = await api.getPackages(...args);
|
||||
if (data?.length === meta.count) {
|
||||
return data;
|
||||
} else if (data) {
|
||||
({ data } = await api.getPackages(...args, meta.count));
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
// call api to list available packages
|
||||
const handlePackagesAvailableSearch = async () => {
|
||||
setFilterAvailable(packagesSearchName);
|
||||
|
||||
const packageList = await getAllPackages();
|
||||
if (packageList) {
|
||||
const packagesAvailableFiltered = filterPackagesAvailable(packageList);
|
||||
setPackagesAvailableSorted(packagesAvailableFiltered, packagesSearchName);
|
||||
setPackagesAvailableFound(
|
||||
packagesAvailableFiltered.length ? true : false
|
||||
);
|
||||
} else {
|
||||
setPackagesAvailable([]);
|
||||
setPackagesAvailableFound(false);
|
||||
}
|
||||
};
|
||||
|
||||
// filter displayed selected packages
|
||||
const handlePackagesChosenSearch = (val) => {
|
||||
let found = false;
|
||||
const filteredPackagesChosen = packagesChosen.map((pack) => {
|
||||
if (!pack.name.includes(val)) {
|
||||
pack.isHidden = true;
|
||||
} else {
|
||||
pack.isHidden = false;
|
||||
found = true;
|
||||
}
|
||||
|
||||
return pack;
|
||||
});
|
||||
|
||||
const setPackagesAvailableSorted = (packageList, filter = filterAvailable) => {
|
||||
const sortResults = packageList.sort(searchResultsComparator(filter));
|
||||
setPackagesAvailable(sortResults);
|
||||
setFilterChosen(val);
|
||||
setPackagesChosenFound(found);
|
||||
setPackagesChosenSorted(filteredPackagesChosen);
|
||||
};
|
||||
|
||||
const keydownHandler = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (focus === 'available') {
|
||||
event.stopPropagation();
|
||||
handlePackagesAvailableSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', keydownHandler, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keydownHandler, true);
|
||||
};
|
||||
});
|
||||
|
||||
const setPackagesChosenSorted = (packageList) => {
|
||||
const sortResults = packageList.sort(searchResultsComparator(filterChosen));
|
||||
setPackagesChosen(sortResults);
|
||||
};
|
||||
const areFound = (filter, packageList) => {
|
||||
if (filter === undefined) {
|
||||
return true;
|
||||
} else if (packageList.some((pack) => pack.name.includes(filter))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// filter the packages by name
|
||||
const filterPackagesAvailable = (packageList) => {
|
||||
return packageList.filter((availablePackage) => {
|
||||
// returns true if no packages in the available or chosen list have the same name
|
||||
return !packagesChosen.some((chosenPackage) => availablePackage.name === chosenPackage.name);
|
||||
});
|
||||
};
|
||||
const isHidden = (filter, pack) =>
|
||||
filter && !pack.name.includes(filter) ? true : false;
|
||||
|
||||
const getAllPackages = async () => {
|
||||
const args = [
|
||||
getState()?.values?.release,
|
||||
getState()?.values?.architecture || defaultArch,
|
||||
packagesSearchName
|
||||
];
|
||||
let { data, meta } = await api.getPackages(...args);
|
||||
if (data?.length === meta.count) {
|
||||
return data;
|
||||
} else if (data) {
|
||||
({ data } = await api.getPackages(...args, meta.count));
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
// call api to list available packages
|
||||
const handlePackagesAvailableSearch = async () => {
|
||||
setFilterAvailable(packagesSearchName);
|
||||
|
||||
const packageList = await getAllPackages();
|
||||
if (packageList) {
|
||||
const packagesAvailableFiltered = filterPackagesAvailable(packageList);
|
||||
setPackagesAvailableSorted(packagesAvailableFiltered, packagesSearchName);
|
||||
setPackagesAvailableFound(packagesAvailableFiltered.length ? true : false);
|
||||
} else {
|
||||
setPackagesAvailable([]);
|
||||
setPackagesAvailableFound(false);
|
||||
}
|
||||
};
|
||||
|
||||
// filter displayed selected packages
|
||||
const handlePackagesChosenSearch = (val) => {
|
||||
let found = false;
|
||||
const filteredPackagesChosen = packagesChosen.map((pack) => {
|
||||
if (!pack.name.includes(val)) {
|
||||
pack.isHidden = true;
|
||||
} else {
|
||||
pack.isHidden = false;
|
||||
found = true;
|
||||
}
|
||||
|
||||
return pack;
|
||||
});
|
||||
|
||||
setFilterChosen(val);
|
||||
setPackagesChosenFound(found);
|
||||
setPackagesChosenSorted(filteredPackagesChosen);
|
||||
};
|
||||
|
||||
const keydownHandler = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (focus === 'available') {
|
||||
event.stopPropagation();
|
||||
handlePackagesAvailableSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', keydownHandler, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keydownHandler, true);
|
||||
};
|
||||
});
|
||||
|
||||
const areFound = (filter, packageList) => {
|
||||
if (filter === undefined) {
|
||||
return true;
|
||||
} else if (packageList.some(pack => pack.name.includes(filter))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isHidden = (filter, pack) => filter && !pack.name.includes(filter) ? true : false;
|
||||
|
||||
const updateState = (updatedPackagesAvailable, updatedPackagesChosen) => {
|
||||
setPackagesChosenSorted(updatedPackagesChosen);
|
||||
setPackagesAvailableSorted(updatedPackagesAvailable);
|
||||
setPackagesAvailableFound(areFound(filterAvailable, updatedPackagesAvailable));
|
||||
setPackagesChosenFound(areFound(filterChosen, updatedPackagesChosen));
|
||||
// set the steps field to the current chosen packages list
|
||||
change(input.name, removePackagesDisplayFields(updatedPackagesChosen));
|
||||
};
|
||||
|
||||
const moveSelectedToChosen = () => {
|
||||
const newPackagesChosen = [];
|
||||
|
||||
const updatedPackagesAvailable = packagesAvailable.filter((pack) => {
|
||||
if (pack.selected) {
|
||||
pack.selected = false;
|
||||
pack.isHidden = isHidden(filterChosen, pack);
|
||||
newPackagesChosen.push(pack);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const updatedPackagesChosen = [ ...newPackagesChosen, ...packagesChosen ];
|
||||
|
||||
updateState(updatedPackagesAvailable, updatedPackagesChosen);
|
||||
};
|
||||
|
||||
const moveSelectedToAvailable = () => {
|
||||
const newPackagesAvailable = [];
|
||||
|
||||
const updatedPackagesChosen = packagesChosen.filter((pack) => {
|
||||
if (pack.selected) {
|
||||
pack.selected = false;
|
||||
pack.isHidden = false;
|
||||
pack.name.includes(filterAvailable) ? newPackagesAvailable.push(pack) : null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const updatedPackagesAvailable = [ ...newPackagesAvailable, ...packagesAvailable ];
|
||||
|
||||
updateState(updatedPackagesAvailable, updatedPackagesChosen);
|
||||
};
|
||||
|
||||
const moveAllToChosen = () => {
|
||||
const newPackagesChosen = packagesAvailable.map(pack => {
|
||||
return { ...pack, selected: false, isHidden: isHidden(filterChosen, pack) };
|
||||
});
|
||||
|
||||
const updatedPackagesAvailable = [];
|
||||
const updatedPackagesChosen = [ ...newPackagesChosen, ...packagesChosen ];
|
||||
|
||||
updateState(updatedPackagesAvailable, updatedPackagesChosen);
|
||||
};
|
||||
|
||||
const moveAllToAvailable = () => {
|
||||
const updatedPackagesChosen = packagesChosen.filter(pack => pack.isHidden);
|
||||
|
||||
const newPackagesAvailable = filterAvailable === undefined ? [] :
|
||||
packagesChosen
|
||||
.filter(pack => !pack.isHidden && pack.name.includes(filterAvailable))
|
||||
.map(pack => { return { ...pack, selected: false };});
|
||||
|
||||
const updatedPackagesAvailable = [ ...newPackagesAvailable, ...packagesAvailable ];
|
||||
|
||||
updateState(updatedPackagesAvailable, updatedPackagesChosen);
|
||||
};
|
||||
|
||||
const onOptionSelect = (event, index, isChosen) => {
|
||||
if (isChosen) {
|
||||
const newChosen = [ ...packagesChosen ];
|
||||
newChosen[index].selected = !packagesChosen[index].selected;
|
||||
setPackagesChosenSorted(newChosen);
|
||||
} else {
|
||||
const newAvailable = [ ...packagesAvailable ];
|
||||
newAvailable[index].selected = !packagesAvailable[index].selected;
|
||||
setPackagesAvailableSorted(newAvailable);
|
||||
}
|
||||
};
|
||||
|
||||
const firstInputElement = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
firstInputElement.current?.focus();
|
||||
}, []);
|
||||
|
||||
const handleClearAvailableSearch = () => {
|
||||
setPackagesSearchName(undefined);
|
||||
setFilterAvailable(undefined);
|
||||
setPackagesAvailable([]);
|
||||
setPackagesAvailableFound(true);
|
||||
};
|
||||
|
||||
const handleClearChosenSearch = () => {
|
||||
setFilterChosen(undefined);
|
||||
setPackagesChosenSorted(packagesChosen.map(pack => {
|
||||
return { ...pack, isHidden: false };}));
|
||||
setPackagesChosenFound(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<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={ handlePackagesAvailableSearch }
|
||||
resetButtonLabel="Clear available packages search"
|
||||
onClear={ handleClearAvailableSearch } /> }>
|
||||
<DualListSelectorList data-testid="available-pkgs-list">
|
||||
{!packagesAvailable.length ? (
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
{!packagesAvailableFound
|
||||
? 'No packages found'
|
||||
: <>Search above to add additional<br />packages to your image</>
|
||||
}
|
||||
</p>
|
||||
) : (packagesAvailable.map((pack, index) => {
|
||||
return !pack.isHidden ? (
|
||||
<DualListSelectorListItem
|
||||
key={ index }
|
||||
isSelected={ pack.selected }
|
||||
onOptionSelect={ (e) => onOptionSelect(e, index, false) }>
|
||||
<TextContent key={ `${pack.name}-${index}` }>
|
||||
<span className="pf-c-dual-list-selector__item-text">{ pack.name }</span>
|
||||
<small>{ pack.summary }</small>
|
||||
</TextContent>
|
||||
</DualListSelectorListItem>
|
||||
) : null;
|
||||
}))}
|
||||
</DualListSelectorList>
|
||||
</DualListSelectorPane>
|
||||
<DualListSelectorControlsWrapper
|
||||
aria-label="Selector controls">
|
||||
<DualListSelectorControl
|
||||
isDisabled={ !packagesAvailable.some(option => option.selected) }
|
||||
onClick={ () => moveSelectedToChosen() }
|
||||
aria-label="Add selected"
|
||||
tooltipContent="Add selected">
|
||||
<AngleRightIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
isDisabled={ !packagesAvailable.length }
|
||||
onClick={ () => moveAllToChosen() }
|
||||
aria-label="Add all"
|
||||
tooltipContent="Add all">
|
||||
<AngleDoubleRightIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
isDisabled={ !packagesChosen.length || !packagesChosenFound }
|
||||
onClick={ () => moveAllToAvailable() }
|
||||
aria-label="Remove all"
|
||||
tooltipContent="Remove all">
|
||||
<AngleDoubleLeftIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
onClick={ () => moveSelectedToAvailable() }
|
||||
isDisabled={ !packagesChosen.some(option => option.selected) || !packagesChosenFound }
|
||||
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) => handlePackagesChosenSearch(val) }
|
||||
resetButtonLabel="Clear chosen packages search"
|
||||
onClear={ handleClearChosenSearch } /> }
|
||||
isChosen>
|
||||
<DualListSelectorList data-testid="chosen-pkgs-list">
|
||||
{!packagesChosen.length ? (
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
No packages added
|
||||
</p>
|
||||
) : !packagesChosenFound ? (
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
No packages found
|
||||
</p>
|
||||
) : (packagesChosen.map((pack, index) => {
|
||||
return !pack.isHidden ? (
|
||||
<DualListSelectorListItem
|
||||
key={ index }
|
||||
isSelected={ pack.selected }
|
||||
onOptionSelect={ (e) => onOptionSelect(e, index, true) }>
|
||||
<TextContent key={ `${pack.name}-${index}` }>
|
||||
<span className="pf-c-dual-list-selector__item-text">{ pack.name }</span>
|
||||
<small>{ pack.summary }</small>
|
||||
</TextContent>
|
||||
</DualListSelectorListItem>
|
||||
) : null;
|
||||
}))}
|
||||
</DualListSelectorList>
|
||||
</DualListSelectorPane>
|
||||
</DualListSelector>
|
||||
const updateState = (updatedPackagesAvailable, updatedPackagesChosen) => {
|
||||
setPackagesChosenSorted(updatedPackagesChosen);
|
||||
setPackagesAvailableSorted(updatedPackagesAvailable);
|
||||
setPackagesAvailableFound(
|
||||
areFound(filterAvailable, updatedPackagesAvailable)
|
||||
);
|
||||
setPackagesChosenFound(areFound(filterChosen, updatedPackagesChosen));
|
||||
// set the steps field to the current chosen packages list
|
||||
change(input.name, removePackagesDisplayFields(updatedPackagesChosen));
|
||||
};
|
||||
|
||||
const moveSelectedToChosen = () => {
|
||||
const newPackagesChosen = [];
|
||||
|
||||
const updatedPackagesAvailable = packagesAvailable.filter((pack) => {
|
||||
if (pack.selected) {
|
||||
pack.selected = false;
|
||||
pack.isHidden = isHidden(filterChosen, pack);
|
||||
newPackagesChosen.push(pack);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const updatedPackagesChosen = [...newPackagesChosen, ...packagesChosen];
|
||||
|
||||
updateState(updatedPackagesAvailable, updatedPackagesChosen);
|
||||
};
|
||||
|
||||
const moveSelectedToAvailable = () => {
|
||||
const newPackagesAvailable = [];
|
||||
|
||||
const updatedPackagesChosen = packagesChosen.filter((pack) => {
|
||||
if (pack.selected) {
|
||||
pack.selected = false;
|
||||
pack.isHidden = false;
|
||||
pack.name.includes(filterAvailable)
|
||||
? newPackagesAvailable.push(pack)
|
||||
: null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const updatedPackagesAvailable = [
|
||||
...newPackagesAvailable,
|
||||
...packagesAvailable,
|
||||
];
|
||||
|
||||
updateState(updatedPackagesAvailable, updatedPackagesChosen);
|
||||
};
|
||||
|
||||
const moveAllToChosen = () => {
|
||||
const newPackagesChosen = packagesAvailable.map((pack) => {
|
||||
return {
|
||||
...pack,
|
||||
selected: false,
|
||||
isHidden: isHidden(filterChosen, pack),
|
||||
};
|
||||
});
|
||||
|
||||
const updatedPackagesAvailable = [];
|
||||
const updatedPackagesChosen = [...newPackagesChosen, ...packagesChosen];
|
||||
|
||||
updateState(updatedPackagesAvailable, updatedPackagesChosen);
|
||||
};
|
||||
|
||||
const moveAllToAvailable = () => {
|
||||
const updatedPackagesChosen = packagesChosen.filter(
|
||||
(pack) => pack.isHidden
|
||||
);
|
||||
|
||||
const newPackagesAvailable =
|
||||
filterAvailable === undefined
|
||||
? []
|
||||
: packagesChosen
|
||||
.filter(
|
||||
(pack) => !pack.isHidden && pack.name.includes(filterAvailable)
|
||||
)
|
||||
.map((pack) => {
|
||||
return { ...pack, selected: false };
|
||||
});
|
||||
|
||||
const updatedPackagesAvailable = [
|
||||
...newPackagesAvailable,
|
||||
...packagesAvailable,
|
||||
];
|
||||
|
||||
updateState(updatedPackagesAvailable, updatedPackagesChosen);
|
||||
};
|
||||
|
||||
const onOptionSelect = (event, index, isChosen) => {
|
||||
if (isChosen) {
|
||||
const newChosen = [...packagesChosen];
|
||||
newChosen[index].selected = !packagesChosen[index].selected;
|
||||
setPackagesChosenSorted(newChosen);
|
||||
} else {
|
||||
const newAvailable = [...packagesAvailable];
|
||||
newAvailable[index].selected = !packagesAvailable[index].selected;
|
||||
setPackagesAvailableSorted(newAvailable);
|
||||
}
|
||||
};
|
||||
|
||||
const firstInputElement = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
firstInputElement.current?.focus();
|
||||
}, []);
|
||||
|
||||
const handleClearAvailableSearch = () => {
|
||||
setPackagesSearchName(undefined);
|
||||
setFilterAvailable(undefined);
|
||||
setPackagesAvailable([]);
|
||||
setPackagesAvailableFound(true);
|
||||
};
|
||||
|
||||
const handleClearChosenSearch = () => {
|
||||
setFilterChosen(undefined);
|
||||
setPackagesChosenSorted(
|
||||
packagesChosen.map((pack) => {
|
||||
return { ...pack, isHidden: false };
|
||||
})
|
||||
);
|
||||
setPackagesChosenFound(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<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={handlePackagesAvailableSearch}
|
||||
resetButtonLabel="Clear available packages search"
|
||||
onClear={handleClearAvailableSearch}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DualListSelectorList data-testid="available-pkgs-list">
|
||||
{!packagesAvailable.length ? (
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
{!packagesAvailableFound ? (
|
||||
'No packages found'
|
||||
) : (
|
||||
<>
|
||||
Search above to add additional
|
||||
<br />
|
||||
packages to your image
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
packagesAvailable.map((pack, index) => {
|
||||
return !pack.isHidden ? (
|
||||
<DualListSelectorListItem
|
||||
key={index}
|
||||
isSelected={pack.selected}
|
||||
onOptionSelect={(e) => onOptionSelect(e, index, false)}
|
||||
>
|
||||
<TextContent key={`${pack.name}-${index}`}>
|
||||
<span className="pf-c-dual-list-selector__item-text">
|
||||
{pack.name}
|
||||
</span>
|
||||
<small>{pack.summary}</small>
|
||||
</TextContent>
|
||||
</DualListSelectorListItem>
|
||||
) : null;
|
||||
})
|
||||
)}
|
||||
</DualListSelectorList>
|
||||
</DualListSelectorPane>
|
||||
<DualListSelectorControlsWrapper aria-label="Selector controls">
|
||||
<DualListSelectorControl
|
||||
isDisabled={!packagesAvailable.some((option) => option.selected)}
|
||||
onClick={() => moveSelectedToChosen()}
|
||||
aria-label="Add selected"
|
||||
tooltipContent="Add selected"
|
||||
>
|
||||
<AngleRightIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
isDisabled={!packagesAvailable.length}
|
||||
onClick={() => moveAllToChosen()}
|
||||
aria-label="Add all"
|
||||
tooltipContent="Add all"
|
||||
>
|
||||
<AngleDoubleRightIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
isDisabled={!packagesChosen.length || !packagesChosenFound}
|
||||
onClick={() => moveAllToAvailable()}
|
||||
aria-label="Remove all"
|
||||
tooltipContent="Remove all"
|
||||
>
|
||||
<AngleDoubleLeftIcon />
|
||||
</DualListSelectorControl>
|
||||
<DualListSelectorControl
|
||||
onClick={() => moveSelectedToAvailable()}
|
||||
isDisabled={
|
||||
!packagesChosen.some((option) => option.selected) ||
|
||||
!packagesChosenFound
|
||||
}
|
||||
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) => handlePackagesChosenSearch(val)}
|
||||
resetButtonLabel="Clear chosen packages search"
|
||||
onClear={handleClearChosenSearch}
|
||||
/>
|
||||
}
|
||||
isChosen
|
||||
>
|
||||
<DualListSelectorList data-testid="chosen-pkgs-list">
|
||||
{!packagesChosen.length ? (
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
No packages added
|
||||
</p>
|
||||
) : !packagesChosenFound ? (
|
||||
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||
No packages found
|
||||
</p>
|
||||
) : (
|
||||
packagesChosen.map((pack, index) => {
|
||||
return !pack.isHidden ? (
|
||||
<DualListSelectorListItem
|
||||
key={index}
|
||||
isSelected={pack.selected}
|
||||
onOptionSelect={(e) => onOptionSelect(e, index, true)}
|
||||
>
|
||||
<TextContent key={`${pack.name}-${index}`}>
|
||||
<span className="pf-c-dual-list-selector__item-text">
|
||||
{pack.name}
|
||||
</span>
|
||||
<small>{pack.summary}</small>
|
||||
</TextContent>
|
||||
</DualListSelectorListItem>
|
||||
) : null;
|
||||
})
|
||||
)}
|
||||
</DualListSelectorList>
|
||||
</DualListSelectorPane>
|
||||
</DualListSelector>
|
||||
);
|
||||
};
|
||||
|
||||
Packages.propTypes = {
|
||||
defaultArch: PropTypes.string,
|
||||
defaultArch: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Packages;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,23 @@ import Radio from '@data-driven-forms/pf4-component-mapper/radio';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
const RadioWithPopover = ({ Popover, ...props }) => {
|
||||
const ref = useRef();
|
||||
return <Radio { ...props } label={ <span ref={ ref } className="ins-c-image--builder__popover">{props.label}
|
||||
<Popover />
|
||||
</span> } />;
|
||||
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
|
||||
Popover: PropTypes.elementType.isRequired,
|
||||
label: PropTypes.node,
|
||||
};
|
||||
|
||||
export default RadioWithPopover;
|
||||
|
|
|
|||
|
|
@ -1,21 +1,33 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
DescriptionList, DescriptionListTerm, DescriptionListGroup, DescriptionListDescription,
|
||||
List, ListItem,
|
||||
Popover,
|
||||
Spinner,
|
||||
Tabs, Tab, TabTitleText,
|
||||
Text, TextContent, TextVariants, TextList, TextListVariants, TextListItem, TextListItemVariants,
|
||||
Button,
|
||||
DescriptionList,
|
||||
DescriptionListTerm,
|
||||
DescriptionListGroup,
|
||||
DescriptionListDescription,
|
||||
List,
|
||||
ListItem,
|
||||
Popover,
|
||||
Spinner,
|
||||
Tabs,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
TextList,
|
||||
TextListVariants,
|
||||
TextListItem,
|
||||
TextListItemVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
TableComposable,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableComposable,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
} from '@patternfly/react-table';
|
||||
import { HelpIcon } from '@patternfly/react-icons';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
|
|
@ -24,308 +36,386 @@ import { RELEASES, UNIT_GIB, UNIT_MIB } from '../../../constants';
|
|||
import isRhel from '../../../Utilities/isRhel';
|
||||
|
||||
const FSReviewTable = ({ ...props }) => {
|
||||
return (
|
||||
<TableComposable
|
||||
aria-label="File system configuration table"
|
||||
variant="compact">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Mount point</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Minimum size</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody data-testid="file-system-configuration-tbody-review">
|
||||
{props.fsc.map((r, ri) =>
|
||||
<Tr key={ ri }>
|
||||
<Td className="pf-m-width-60">{ r.mountpoint }</Td>
|
||||
<Td className="pf-m-width-10">xfs</Td>
|
||||
<Td className="pf-m-width-30">{ r.size } { r.unit === UNIT_GIB ? 'GiB' : r.unit === UNIT_MIB ? 'MiB' : 'KiB' }</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
);
|
||||
return (
|
||||
<TableComposable
|
||||
aria-label="File system configuration table"
|
||||
variant="compact"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Mount point</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Minimum size</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody data-testid="file-system-configuration-tbody-review">
|
||||
{props.fsc.map((r, ri) => (
|
||||
<Tr key={ri}>
|
||||
<Td className="pf-m-width-60">{r.mountpoint}</Td>
|
||||
<Td className="pf-m-width-10">xfs</Td>
|
||||
<Td className="pf-m-width-30">
|
||||
{r.size}{' '}
|
||||
{r.unit === UNIT_GIB
|
||||
? 'GiB'
|
||||
: r.unit === UNIT_MIB
|
||||
? 'MiB'
|
||||
: 'KiB'}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
);
|
||||
};
|
||||
|
||||
FSReviewTable.propTypes = {
|
||||
fsc: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fsc: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
const ReviewStep = () => {
|
||||
const [ activeTabKey, setActiveTabKey ] = useState(0);
|
||||
const [ orgId, setOrgId ] = useState();
|
||||
const [ minSize, setMinSize ] = useState();
|
||||
const { change, getState } = useFormApi();
|
||||
const [activeTabKey, setActiveTabKey] = useState(0);
|
||||
const [orgId, setOrgId] = useState();
|
||||
const [minSize, setMinSize] = useState();
|
||||
const { change, getState } = useFormApi();
|
||||
|
||||
useEffect(() => {
|
||||
const registerSystem = getState()?.values?.['register-system'];
|
||||
if (registerSystem === 'register-now' || registerSystem === 'register-now-insights') {
|
||||
(async () => {
|
||||
const userData = await insights?.chrome?.auth?.getUser();
|
||||
const id = userData?.identity?.internal?.org_id;
|
||||
setOrgId(id);
|
||||
change('subscription-organization-id', id);
|
||||
})();
|
||||
}
|
||||
useEffect(() => {
|
||||
const registerSystem = getState()?.values?.['register-system'];
|
||||
if (
|
||||
registerSystem === 'register-now' ||
|
||||
registerSystem === 'register-now-insights'
|
||||
) {
|
||||
(async () => {
|
||||
const userData = await insights?.chrome?.auth?.getUser();
|
||||
const id = userData?.identity?.internal?.org_id;
|
||||
setOrgId(id);
|
||||
change('subscription-organization-id', id);
|
||||
})();
|
||||
}
|
||||
|
||||
if (getState()?.values?.['file-system-config-toggle'] === 'manual' &&
|
||||
getState()?.values?.['file-system-configuration']) {
|
||||
let size = 0;
|
||||
for (const fsc of getState().values['file-system-configuration']) {
|
||||
size += (fsc.size * fsc.unit);
|
||||
}
|
||||
if (
|
||||
getState()?.values?.['file-system-config-toggle'] === 'manual' &&
|
||||
getState()?.values?.['file-system-configuration']
|
||||
) {
|
||||
let size = 0;
|
||||
for (const fsc of getState().values['file-system-configuration']) {
|
||||
size += fsc.size * fsc.unit;
|
||||
}
|
||||
|
||||
size = (size / UNIT_GIB).toFixed(1);
|
||||
if (size < 1) {
|
||||
setMinSize(`Less than 1 GiB`);
|
||||
} else {
|
||||
setMinSize(`${size} GiB`);
|
||||
}
|
||||
}
|
||||
});
|
||||
size = (size / UNIT_GIB).toFixed(1);
|
||||
if (size < 1) {
|
||||
setMinSize(`Less than 1 GiB`);
|
||||
} else {
|
||||
setMinSize(`${size} GiB`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleTabClick = (event, tabIndex) => {
|
||||
setActiveTabKey(tabIndex);
|
||||
};
|
||||
const handleTabClick = (event, tabIndex) => {
|
||||
setActiveTabKey(tabIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Review the information and click "Create image"
|
||||
to create the image using the following criteria.
|
||||
</Text>
|
||||
<DescriptionList isCompact isHorizontal>
|
||||
<DescriptionListGroup>
|
||||
{getState()?.values?.['image-name'] &&
|
||||
<>
|
||||
<DescriptionListTerm>Image name</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{getState()?.values?.['image-name']}
|
||||
</DescriptionListDescription>
|
||||
</>
|
||||
}
|
||||
<DescriptionListTerm>Release</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{RELEASES[getState()?.values?.release]}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
<Tabs isFilled activeKey={ activeTabKey } onSelect={ handleTabClick } className="pf-u-w-75">
|
||||
<Tab eventKey={ 0 } title={ <TabTitleText>Target environment</TabTitleText> } data-testid='tab-target' autoFocus>
|
||||
<List isPlain iconSize="large">
|
||||
{getState()?.values?.['target-environment']?.aws &&
|
||||
<ListItem icon={ <img className='provider-icon' src='/apps/frontend-assets/partners-icons/aws.svg' /> }>
|
||||
<TextContent>
|
||||
<Text component={ TextVariants.h3 }>
|
||||
Amazon Web Services
|
||||
</Text>
|
||||
<TextList component={ TextListVariants.dl }>
|
||||
<TextListItem component={ TextListItemVariants.dt }>Account ID</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
{getState()?.values?.['aws-account-id']}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
}
|
||||
{getState()?.values?.['target-environment']?.gcp &&
|
||||
<ListItem
|
||||
className='pf-c-list__item pf-u-mt-md'
|
||||
icon={ <img className='provider-icon' src='/apps/frontend-assets/partners-icons/google-cloud-short.svg' /> }>
|
||||
<TextContent>
|
||||
<Text component={ TextVariants.h3 }>Google Cloud Platform</Text>
|
||||
<TextList component={ TextListVariants.dl }>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
{googleAccType?.[getState()?.values?.['google-account-type']]}
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
{getState()?.values?.['google-email'] || getState()?.values?.['google-domain']}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
}
|
||||
{getState()?.values?.['target-environment']?.azure &&
|
||||
<ListItem
|
||||
className='pf-c-list__item pf-u-mt-md'
|
||||
icon={ <img className='provider-icon' src='/apps/frontend-assets/partners-icons/microsoft-azure-short.svg' /> }>
|
||||
<TextContent>
|
||||
<Text component={ TextVariants.h3 }>Microsoft Azure</Text>
|
||||
<TextList component={ TextListVariants.dl }>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
Subscription ID
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
{getState()?.values?.['azure-subscription-id']}
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
Tenant ID
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
{getState()?.values?.['azure-tenant-id']}
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
Resource group
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
{getState()?.values?.['azure-resource-group']}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
}
|
||||
{getState()?.values?.['target-environment']?.vsphere &&
|
||||
<ListItem>
|
||||
<TextContent>
|
||||
<Text component={ TextVariants.h3 }>
|
||||
VMWare
|
||||
</Text>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
}
|
||||
{getState()?.values?.['target-environment']?.['guest-image'] &&
|
||||
<ListItem>
|
||||
<TextContent>
|
||||
<Text component={ TextVariants.h3 }>
|
||||
Virtualization - Guest image
|
||||
</Text>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
}
|
||||
{getState()?.values?.['target-environment']?.['image-installer'] &&
|
||||
<ListItem>
|
||||
<TextContent>
|
||||
<Text component={ TextVariants.h3 }>
|
||||
Bare metal - Installer
|
||||
</Text>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
}
|
||||
</List>
|
||||
</Tab>
|
||||
{isRhel(getState()?.values?.release) &&
|
||||
<Tab eventKey={ 1 } title={ <TabTitleText>Registration</TabTitleText> } data-testid='tab-registration'>
|
||||
{getState()?.values?.['register-system'] === 'register-later' &&
|
||||
<TextContent>
|
||||
<TextList component={ TextListVariants.dl }>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
Subscription
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
Register the system later
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
}
|
||||
{(getState()?.values?.['register-system'] === 'register-now' ||
|
||||
getState()?.values?.['register-system'] === 'register-now-insights') &&
|
||||
<TextContent>
|
||||
<TextList component={ TextListVariants.dl }>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
Subscription
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
{getState()?.values?.['register-system'] === 'register-now-insights' &&
|
||||
'Register with Subscriptions and Red Hat Insights'
|
||||
}
|
||||
{getState()?.values?.['register-system'] === 'register-now' &&
|
||||
'Register with Subscriptions'
|
||||
}
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
Activation key
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
{getState()?.values?.['subscription-activation-key']}
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
Organization ID
|
||||
</TextListItem>
|
||||
{orgId !== undefined ? (
|
||||
<TextListItem component={ TextListItemVariants.dd } data-testid='organization-id'>
|
||||
{orgId}
|
||||
</TextListItem>
|
||||
) : (
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
<Spinner />
|
||||
</TextListItem>
|
||||
)}
|
||||
</TextList>
|
||||
</TextContent>
|
||||
}
|
||||
</Tab>
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Review the information and click "Create image" to create the
|
||||
image using the following criteria.
|
||||
</Text>
|
||||
<DescriptionList isCompact isHorizontal>
|
||||
<DescriptionListGroup>
|
||||
{getState()?.values?.['image-name'] && (
|
||||
<>
|
||||
<DescriptionListTerm>Image name</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{getState()?.values?.['image-name']}
|
||||
</DescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
<DescriptionListTerm>Release</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{RELEASES[getState()?.values?.release]}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
<Tabs
|
||||
isFilled
|
||||
activeKey={activeTabKey}
|
||||
onSelect={handleTabClick}
|
||||
className="pf-u-w-75"
|
||||
>
|
||||
<Tab
|
||||
eventKey={0}
|
||||
title={<TabTitleText>Target environment</TabTitleText>}
|
||||
data-testid="tab-target"
|
||||
autoFocus
|
||||
>
|
||||
<List isPlain iconSize="large">
|
||||
{getState()?.values?.['target-environment']?.aws && (
|
||||
<ListItem
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src="/apps/frontend-assets/partners-icons/aws.svg"
|
||||
/>
|
||||
}
|
||||
<Tab eventKey={ 2 } title={ <TabTitleText>System configuration</TabTitleText> } data-testid='tab-system'>
|
||||
<TextContent>
|
||||
<Text component={ TextVariants.h3 }>File system configuration</Text>
|
||||
<TextList component={ TextListVariants.dl }>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
Partitioning
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd } data-testid='partitioning-auto-manual'>
|
||||
{getState()?.values?.['file-system-config-toggle'] === 'manual' ? 'Manual' : 'Automatic'}
|
||||
{getState()?.values?.['file-system-config-toggle'] === 'manual' &&
|
||||
<>
|
||||
{' '}
|
||||
<Popover
|
||||
position="bottom"
|
||||
headerContent="Partitions"
|
||||
hasAutoWidth
|
||||
minWidth="30rem"
|
||||
bodyContent={ <FSReviewTable fsc={ getState().values['file-system-configuration'] } /> }>
|
||||
<Button
|
||||
data-testid='file-system-configuration-popover'
|
||||
variant="link"
|
||||
aria-label="File system configuration info"
|
||||
aria-describedby="file-system-configuration-info">
|
||||
View partitions
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
</TextListItem>
|
||||
{getState()?.values?.['file-system-config-toggle'] === 'manual' &&
|
||||
<>
|
||||
<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>
|
||||
<TextListItem component={ TextListItemVariants.dd }>
|
||||
{ minSize }
|
||||
</TextListItem>
|
||||
</>
|
||||
}
|
||||
</TextList>
|
||||
<Text component={ TextVariants.h3 }>Packages</Text>
|
||||
<TextList component={ TextListVariants.dl }>
|
||||
<TextListItem component={ TextListItemVariants.dt }>
|
||||
Chosen
|
||||
</TextListItem>
|
||||
<TextListItem component={ TextListItemVariants.dd } data-testid='chosen-packages-count'>
|
||||
{getState()?.values?.['selected-packages']?.length || 0}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Amazon Web Services</Text>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Account ID
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['aws-account-id']}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.gcp && (
|
||||
<ListItem
|
||||
className="pf-c-list__item pf-u-mt-md"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src="/apps/frontend-assets/partners-icons/google-cloud-short.svg"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Google Cloud Platform</Text>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
{
|
||||
googleAccType?.[
|
||||
getState()?.values?.['google-account-type']
|
||||
]
|
||||
}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['google-email'] ||
|
||||
getState()?.values?.['google-domain']}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.azure && (
|
||||
<ListItem
|
||||
className="pf-c-list__item pf-u-mt-md"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src="/apps/frontend-assets/partners-icons/microsoft-azure-short.svg"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Microsoft Azure</Text>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Subscription ID
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['azure-subscription-id']}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Tenant ID
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['azure-tenant-id']}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Resource group
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['azure-resource-group']}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.vsphere && (
|
||||
<ListItem>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>VMWare</Text>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.['guest-image'] && (
|
||||
<ListItem>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>
|
||||
Virtualization - Guest image
|
||||
</Text>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
)}
|
||||
{getState()?.values?.['target-environment']?.[
|
||||
'image-installer'
|
||||
] && (
|
||||
<ListItem>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>
|
||||
Bare metal - Installer
|
||||
</Text>
|
||||
</TextContent>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Tab>
|
||||
{isRhel(getState()?.values?.release) && (
|
||||
<Tab
|
||||
eventKey={1}
|
||||
title={<TabTitleText>Registration</TabTitleText>}
|
||||
data-testid="tab-registration"
|
||||
>
|
||||
{getState()?.values?.['register-system'] === 'register-later' && (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Subscription
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
Register the system later
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
)}
|
||||
{(getState()?.values?.['register-system'] === 'register-now' ||
|
||||
getState()?.values?.['register-system'] ===
|
||||
'register-now-insights') && (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Subscription
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['register-system'] ===
|
||||
'register-now-insights' &&
|
||||
'Register with Subscriptions and Red Hat Insights'}
|
||||
{getState()?.values?.['register-system'] ===
|
||||
'register-now' && 'Register with Subscriptions'}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Activation key
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{getState()?.values?.['subscription-activation-key']}
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Organization ID
|
||||
</TextListItem>
|
||||
{orgId !== undefined ? (
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dd}
|
||||
data-testid="organization-id"
|
||||
>
|
||||
{orgId}
|
||||
</TextListItem>
|
||||
) : (
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
<Spinner />
|
||||
</TextListItem>
|
||||
)}
|
||||
</TextList>
|
||||
</TextContent>
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
<Tab
|
||||
eventKey={2}
|
||||
title={<TabTitleText>System configuration</TabTitleText>}
|
||||
data-testid="tab-system"
|
||||
>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>File system configuration</Text>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Partitioning
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dd}
|
||||
data-testid="partitioning-auto-manual"
|
||||
>
|
||||
{getState()?.values?.['file-system-config-toggle'] === 'manual'
|
||||
? 'Manual'
|
||||
: 'Automatic'}
|
||||
{getState()?.values?.['file-system-config-toggle'] ===
|
||||
'manual' && (
|
||||
<>
|
||||
{' '}
|
||||
<Popover
|
||||
position="bottom"
|
||||
headerContent="Partitions"
|
||||
hasAutoWidth
|
||||
minWidth="30rem"
|
||||
bodyContent={
|
||||
<FSReviewTable
|
||||
fsc={getState().values['file-system-configuration']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
data-testid="file-system-configuration-popover"
|
||||
variant="link"
|
||||
aria-label="File system configuration info"
|
||||
aria-describedby="file-system-configuration-info"
|
||||
>
|
||||
View partitions
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</TextListItem>
|
||||
{getState()?.values?.['file-system-config-toggle'] ===
|
||||
'manual' && (
|
||||
<>
|
||||
<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>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{minSize}
|
||||
</TextListItem>
|
||||
</>
|
||||
)}
|
||||
</TextList>
|
||||
<Text component={TextVariants.h3}>Packages</Text>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Chosen
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dd}
|
||||
data-testid="chosen-packages-count"
|
||||
>
|
||||
{getState()?.values?.['selected-packages']?.length || 0}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReviewStep;
|
||||
|
|
|
|||
|
|
@ -1,74 +1,75 @@
|
|||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
TextInput,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { UNIT_KIB, UNIT_MIB, UNIT_GIB } from '../../../constants';
|
||||
|
||||
const SizeUnit = ({ ...props }) => {
|
||||
const [ isOpen, setIsOpen ] = useState(false);
|
||||
const [ unit, setUnit ] = useState(props.unit || UNIT_GIB);
|
||||
const [ size, setSize ] = useState(props.size || 1);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [unit, setUnit] = useState(props.unit || UNIT_GIB);
|
||||
const [size, setSize] = useState(props.size || 1);
|
||||
|
||||
useEffect(() => {
|
||||
props.onChange(size, unit);
|
||||
}, [ unit, size ]);
|
||||
useEffect(() => {
|
||||
props.onChange(size, unit);
|
||||
}, [unit, size]);
|
||||
|
||||
const onToggle = (isOpen) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
const onToggle = (isOpen) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
const onSelect = (event, selection) => {
|
||||
switch (selection) {
|
||||
case 'KiB':
|
||||
setUnit(UNIT_KIB);
|
||||
break;
|
||||
case 'MiB':
|
||||
setUnit(UNIT_MIB);
|
||||
break;
|
||||
case 'GiB':
|
||||
setUnit(UNIT_GIB);
|
||||
break;
|
||||
const onSelect = (event, selection) => {
|
||||
switch (selection) {
|
||||
case 'KiB':
|
||||
setUnit(UNIT_KIB);
|
||||
break;
|
||||
case 'MiB':
|
||||
setUnit(UNIT_MIB);
|
||||
break;
|
||||
case 'GiB':
|
||||
setUnit(UNIT_GIB);
|
||||
break;
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
className="pf-u-w-50"
|
||||
type="text"
|
||||
value={size}
|
||||
aria-label="Size text input"
|
||||
onChange={(v) => setSize(isNaN(parseInt(v)) ? 0 : parseInt(v))}
|
||||
/>
|
||||
<Select
|
||||
className="pf-u-w-50"
|
||||
isOpen={isOpen}
|
||||
onToggle={onToggle}
|
||||
onSelect={onSelect}
|
||||
selections={
|
||||
unit === UNIT_KIB ? 'KiB' : unit === UNIT_MIB ? 'MiB' : 'GiB'
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
className="pf-u-w-50"
|
||||
type="text"
|
||||
value={ size }
|
||||
aria-label="Size text input"
|
||||
onChange={ v => setSize(isNaN(parseInt(v)) ? 0 : parseInt(v)) } />
|
||||
<Select
|
||||
className="pf-u-w-50"
|
||||
isOpen={ isOpen }
|
||||
onToggle={ onToggle }
|
||||
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>
|
||||
</>
|
||||
);
|
||||
variant={SelectVariant.single}
|
||||
aria-label="Unit select"
|
||||
>
|
||||
{['KiB', 'MiB', 'GiB'].map((u, index) => {
|
||||
return <SelectOption key={index} value={u} />;
|
||||
})}
|
||||
</Select>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SizeUnit.propTypes = {
|
||||
size: PropTypes.number.isRequired,
|
||||
unit: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
unit: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SizeUnit;
|
||||
|
|
|
|||
|
|
@ -2,125 +2,164 @@ import React, { useState, useEffect } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import { Checkbox, FormGroup, Text, TextVariants, Tile } from '@patternfly/react-core';
|
||||
import {
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
Text,
|
||||
TextVariants,
|
||||
Tile,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
const TargetEnvironment = ({ label, isRequired, ...props }) => {
|
||||
const { getState, change } = useFormApi();
|
||||
const { input } = useFieldApi({ label, isRequired, ...props });
|
||||
const [ environment, setEnvironment ] = useState({
|
||||
aws: false,
|
||||
azure: false,
|
||||
gcp: false,
|
||||
vsphere: false,
|
||||
'guest-image': false,
|
||||
'image-installer': false,
|
||||
const { getState, change } = useFormApi();
|
||||
const { input } = useFieldApi({ label, isRequired, ...props });
|
||||
const [environment, setEnvironment] = useState({
|
||||
aws: false,
|
||||
azure: false,
|
||||
gcp: false,
|
||||
vsphere: false,
|
||||
'guest-image': false,
|
||||
'image-installer': false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (getState()?.values?.[input.name]) {
|
||||
setEnvironment(getState().values[input.name]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSetEnvironment = (env) =>
|
||||
setEnvironment((prevEnv) => {
|
||||
const newEnv = {
|
||||
...prevEnv,
|
||||
[env]: !prevEnv[env],
|
||||
};
|
||||
change(input.name, newEnv);
|
||||
return newEnv;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (getState()?.values?.[input.name]) {
|
||||
setEnvironment(getState().values[input.name]);
|
||||
}
|
||||
}, []);
|
||||
const handleKeyDown = (e, env) => {
|
||||
if (e.key === ' ') {
|
||||
handleSetEnvironment(env);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetEnvironment = (env) => setEnvironment((prevEnv) => {
|
||||
const newEnv = ({
|
||||
...prevEnv,
|
||||
[env]: !prevEnv[env]
|
||||
});
|
||||
change(input.name, newEnv);
|
||||
return newEnv;
|
||||
});
|
||||
|
||||
const handleKeyDown = (e, env) => {
|
||||
if (e.key === ' ') {
|
||||
handleSetEnvironment(env);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-aws"
|
||||
title="Amazon Web Services"
|
||||
icon={ <img
|
||||
className='provider-icon'
|
||||
src={ '/apps/frontend-assets/partners-icons/aws.svg' } /> }
|
||||
onClick={ () => handleSetEnvironment('aws') }
|
||||
onKeyDown = { (e) => handleKeyDown(e, 'aws') }
|
||||
isSelected={ environment.aws }
|
||||
isStacked
|
||||
isDisplayLarge />
|
||||
<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' } /> }
|
||||
onClick={ () => handleSetEnvironment('gcp') }
|
||||
isSelected={ environment.gcp }
|
||||
onKeyDown = { (e) => handleKeyDown(e, 'gcp') }
|
||||
isStacked
|
||||
isDisplayLarge />
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-azure"
|
||||
title="Microsoft Azure"
|
||||
icon={ <img
|
||||
className='provider-icon'
|
||||
src={ '/apps/frontend-assets/partners-icons/microsoft-azure-short.svg' } /> }
|
||||
onClick={ () => handleSetEnvironment('azure') }
|
||||
onKeyDown = { (e) => handleKeyDown(e, 'azure') }
|
||||
isSelected={ environment.azure }
|
||||
isStacked
|
||||
isDisplayLarge />
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup label={ <Text component={ TextVariants.small }>Private cloud</Text> } data-testid="target-private">
|
||||
<Checkbox
|
||||
label="VMWare"
|
||||
isChecked={ environment.vsphere }
|
||||
onChange={ () => handleSetEnvironment('vsphere') }
|
||||
aria-label="VMWare checkbox"
|
||||
id="checkbox-vmware"
|
||||
name="VMWare"
|
||||
data-testid="checkbox-vmware" />
|
||||
</FormGroup>
|
||||
<FormGroup label={ <Text component={ TextVariants.small }>Other</Text> } data-testid="target-other">
|
||||
<Checkbox
|
||||
label="Virtualization - Guest image"
|
||||
isChecked={ environment['guest-image'] }
|
||||
onChange={ () => handleSetEnvironment('guest-image') }
|
||||
aria-label="Virtualization guest image checkbox"
|
||||
id="checkbox-guest-image"
|
||||
name="Virtualization guest image"
|
||||
data-testid="checkbox-guest-image" />
|
||||
<Checkbox
|
||||
label="Bare metal - Installer"
|
||||
isChecked={ environment['image-installer'] }
|
||||
onChange={ () => handleSetEnvironment('image-installer') }
|
||||
aria-label="Bare metal installer checkbox"
|
||||
id="checkbox-image-installer"
|
||||
name="Bare metal installer"
|
||||
data-testid="checkbox-image-installer" />
|
||||
</FormGroup>
|
||||
</FormGroup>
|
||||
</>
|
||||
);
|
||||
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">
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-aws"
|
||||
title="Amazon Web Services"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={'/apps/frontend-assets/partners-icons/aws.svg'}
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('aws')}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'aws')}
|
||||
isSelected={environment.aws}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
<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'
|
||||
}
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('gcp')}
|
||||
isSelected={environment.gcp}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'gcp')}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-azure"
|
||||
title="Microsoft Azure"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={
|
||||
'/apps/frontend-assets/partners-icons/microsoft-azure-short.svg'
|
||||
}
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('azure')}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'azure')}
|
||||
isSelected={environment.azure}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Private cloud</Text>}
|
||||
data-testid="target-private"
|
||||
>
|
||||
<Checkbox
|
||||
label="VMWare"
|
||||
isChecked={environment.vsphere}
|
||||
onChange={() => handleSetEnvironment('vsphere')}
|
||||
aria-label="VMWare checkbox"
|
||||
id="checkbox-vmware"
|
||||
name="VMWare"
|
||||
data-testid="checkbox-vmware"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Other</Text>}
|
||||
data-testid="target-other"
|
||||
>
|
||||
<Checkbox
|
||||
label="Virtualization - Guest image"
|
||||
isChecked={environment['guest-image']}
|
||||
onChange={() => handleSetEnvironment('guest-image')}
|
||||
aria-label="Virtualization guest image checkbox"
|
||||
id="checkbox-guest-image"
|
||||
name="Virtualization guest image"
|
||||
data-testid="checkbox-guest-image"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Bare metal - Installer"
|
||||
isChecked={environment['image-installer']}
|
||||
onChange={() => handleSetEnvironment('image-installer')}
|
||||
aria-label="Bare metal installer checkbox"
|
||||
id="checkbox-image-installer"
|
||||
name="Bare metal installer"
|
||||
data-testid="checkbox-image-installer"
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TargetEnvironment.propTypes = {
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
||||
TargetEnvironment.defaultProps = {
|
||||
label: '',
|
||||
isRequired: false
|
||||
label: '',
|
||||
isRequired: false,
|
||||
};
|
||||
|
||||
export default TargetEnvironment;
|
||||
|
|
|
|||
|
|
@ -6,40 +6,47 @@ import { Title } from '@patternfly/react-core';
|
|||
import StepTemplate from './stepTemplate';
|
||||
|
||||
export default {
|
||||
StepTemplate,
|
||||
id: 'wizard-target-aws',
|
||||
title: 'Amazon Web Services',
|
||||
customTitle: <Title headingLevel="h1" size="xl">Target environment - Amazon Web Service</Title>,
|
||||
name: 'aws-target-env',
|
||||
substepOf: 'Target environment',
|
||||
nextStep: ({ values }) => nextStepMapper(values, { skipAws: true }),
|
||||
fields: [
|
||||
StepTemplate,
|
||||
id: 'wizard-target-aws',
|
||||
title: 'Amazon Web Services',
|
||||
customTitle: (
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Target environment - Amazon Web Service
|
||||
</Title>
|
||||
),
|
||||
name: 'aws-target-env',
|
||||
substepOf: 'Target environment',
|
||||
nextStep: ({ values }) => nextStepMapper(values, { skipAws: true }),
|
||||
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. <br />
|
||||
The image should be copied to your account within 14 days.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'aws-account-id',
|
||||
className: 'pf-u-w-25',
|
||||
'data-testid': 'aws-account-id',
|
||||
type: 'text',
|
||||
label: 'AWS account ID',
|
||||
isRequired: true,
|
||||
autoFocus: true,
|
||||
validate: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'plain-text-component',
|
||||
label: <p>
|
||||
Your image will be uploaded to AWS and shared with the account you provide below. <br />
|
||||
The image should be copied to your account within 14 days.
|
||||
</p>
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'aws-account-id',
|
||||
className: 'pf-u-w-25',
|
||||
'data-testid': 'aws-account-id',
|
||||
type: 'text',
|
||||
label: 'AWS account ID',
|
||||
isRequired: true,
|
||||
autoFocus: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
type: validatorTypes.EXACT_LENGTH,
|
||||
threshold: 12
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
type: validatorTypes.EXACT_LENGTH,
|
||||
threshold: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,37 +6,46 @@ import { Text } from '@patternfly/react-core';
|
|||
import StepTemplate from './stepTemplate';
|
||||
|
||||
export default {
|
||||
StepTemplate,
|
||||
id: 'wizard-systemconfiguration-filesystem',
|
||||
title: 'File system configuration',
|
||||
name: 'File system configuration',
|
||||
substepOf: 'System Configuration',
|
||||
nextStep: 'packages',
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'file-system-configuration-text-component',
|
||||
label: <>
|
||||
<Text>Red Hat recommends using automatic partitioning for most installations.</Text>
|
||||
<Text>
|
||||
Alternatively, you may manually configure the file system of your image by adding, removing, and editing partitions.
|
||||
</Text>
|
||||
</>,
|
||||
},
|
||||
{
|
||||
component: 'file-system-config-toggle',
|
||||
name: 'file-system-config-toggle',
|
||||
label: 'File system configurations toggle',
|
||||
},
|
||||
{
|
||||
component: 'file-system-configuration',
|
||||
name: 'file-system-configuration',
|
||||
label: 'File system configurations',
|
||||
validate: [{ type: 'fileSystemConfigurationValidator' }, { type: validatorTypes.REQUIRED }],
|
||||
condition: {
|
||||
when: 'file-system-config-toggle',
|
||||
is: 'manual',
|
||||
},
|
||||
},
|
||||
]
|
||||
StepTemplate,
|
||||
id: 'wizard-systemconfiguration-filesystem',
|
||||
title: 'File system configuration',
|
||||
name: 'File system configuration',
|
||||
substepOf: 'System Configuration',
|
||||
nextStep: 'packages',
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'file-system-configuration-text-component',
|
||||
label: (
|
||||
<>
|
||||
<Text>
|
||||
Red Hat recommends using automatic partitioning for most
|
||||
installations.
|
||||
</Text>
|
||||
<Text>
|
||||
Alternatively, you may manually configure the file system of your
|
||||
image by adding, removing, and editing partitions.
|
||||
</Text>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'file-system-config-toggle',
|
||||
name: 'file-system-config-toggle',
|
||||
label: 'File system configurations toggle',
|
||||
},
|
||||
{
|
||||
component: 'file-system-configuration',
|
||||
name: 'file-system-configuration',
|
||||
label: 'File system configurations',
|
||||
validate: [
|
||||
{ type: 'fileSystemConfigurationValidator' },
|
||||
{ type: validatorTypes.REQUIRED },
|
||||
],
|
||||
condition: {
|
||||
when: 'file-system-config-toggle',
|
||||
is: 'manual',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,136 +3,168 @@ import componentTypes from '@data-driven-forms/react-form-renderer/component-typ
|
|||
import validatorTypes from '@data-driven-forms/react-form-renderer/validator-types';
|
||||
import { HelpIcon } from '@patternfly/react-icons';
|
||||
import nextStepMapper from './imageOutputStepMapper';
|
||||
import { Title, Text, Popover, TextContent, TextList, TextListItem, Button } from '@patternfly/react-core';
|
||||
import {
|
||||
Title,
|
||||
Text,
|
||||
Popover,
|
||||
TextContent,
|
||||
TextList,
|
||||
TextListItem,
|
||||
Button,
|
||||
} from '@patternfly/react-core';
|
||||
import PropTypes from 'prop-types';
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
export const googleAccType = {
|
||||
googleAccount: 'Google account',
|
||||
serviceAccount: 'Service account',
|
||||
googleGroup: 'Google group',
|
||||
domain: 'Domain'
|
||||
googleAccount: 'Google account',
|
||||
serviceAccount: 'Service account',
|
||||
googleGroup: 'Google group',
|
||||
domain: 'Domain',
|
||||
};
|
||||
|
||||
const PopoverInfo = ({ appendTo }) => {
|
||||
return <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. e.g., <em>`alice@gmail.com`</em>.
|
||||
</TextListItem>
|
||||
<TextListItem>
|
||||
<strong>Service account:</strong> A service account is an account for an application instead
|
||||
of an individual end user. e.g., <em>`myapp@appspot.gserviceaccount.com`</em>.
|
||||
</TextListItem>
|
||||
<TextListItem>
|
||||
<strong>Google group:</strong> A Google group is a named collection of Google accounts and
|
||||
service accounts. e.g., <em>`admins@example.com`</em>.
|
||||
</TextListItem>
|
||||
<TextListItem>
|
||||
<strong>Google Workspace domain/Cloud Identity domain:</strong> A Google workspace or cloud identity
|
||||
domain represents a virtual group of all the Google accounts in an organization. These domains
|
||||
represent your organization's internet domain name. e.g., <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>;
|
||||
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. e.g., <em>`alice@gmail.com`</em>.
|
||||
</TextListItem>
|
||||
<TextListItem>
|
||||
<strong>Service account:</strong> A service account is an account
|
||||
for an application instead of an individual end user. e.g.,{' '}
|
||||
<em>`myapp@appspot.gserviceaccount.com`</em>.
|
||||
</TextListItem>
|
||||
<TextListItem>
|
||||
<strong>Google group:</strong> A Google group is a named
|
||||
collection of Google accounts and service accounts. e.g.,{' '}
|
||||
<em>`admins@example.com`</em>.
|
||||
</TextListItem>
|
||||
<TextListItem>
|
||||
<strong>Google Workspace domain/Cloud Identity domain:</strong> A
|
||||
Google workspace or cloud identity domain represents a virtual
|
||||
group of all the Google accounts in an organization. These domains
|
||||
represent your organization's internet domain name. e.g.,{' '}
|
||||
<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
|
||||
appendTo: PropTypes.any,
|
||||
};
|
||||
|
||||
export default {
|
||||
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 }),
|
||||
fields: [
|
||||
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 }),
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'google-cloud-text-component',
|
||||
label: (
|
||||
<Text>
|
||||
Your image will be uploaded to Google Cloud Platform and shared with
|
||||
the email you provide below. <br />
|
||||
The image should be copied to your account within 14 days.
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'radio-popover',
|
||||
label: 'Type',
|
||||
isRequired: true,
|
||||
Popover: PopoverInfo,
|
||||
name: 'google-account-type',
|
||||
initialValue: 'googleAccount',
|
||||
options: Object.entries(googleAccType).map(([value, label]) => ({
|
||||
label:
|
||||
value === 'domain'
|
||||
? 'Google Workspace domain or Cloud Identity domain'
|
||||
: label,
|
||||
value,
|
||||
autoFocus: value === 'googleAccount' ? true : false,
|
||||
})),
|
||||
validate: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'google-cloud-text-component',
|
||||
label: <Text>
|
||||
Your image will be uploaded to Google Cloud Platform and shared with the email you provide below. <br />
|
||||
The image should be copied to your account within 14 days.
|
||||
</Text>
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'google-email',
|
||||
'data-testid': 'input-google-email',
|
||||
type: 'text',
|
||||
label: 'Email address',
|
||||
condition: {
|
||||
or: [
|
||||
{ when: 'google-account-type', is: 'googleAccount' },
|
||||
{ when: 'google-account-type', is: 'serviceAccount' },
|
||||
{ when: 'google-account-type', is: 'googleGroup' },
|
||||
{ when: 'google-account-type', is: null },
|
||||
],
|
||||
},
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
component: 'radio-popover',
|
||||
label: 'Type',
|
||||
isRequired: true,
|
||||
Popover: PopoverInfo,
|
||||
name: 'google-account-type',
|
||||
initialValue: 'googleAccount',
|
||||
options: Object.entries(googleAccType).map(([ value, label ]) => ({
|
||||
label: value === 'domain' ? 'Google Workspace domain or Cloud Identity domain' : label,
|
||||
value,
|
||||
autoFocus: value === 'googleAccount' ? true : false
|
||||
})),
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
type: validatorTypes.PATTERN,
|
||||
pattern: '[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,}$',
|
||||
message: 'Please enter a valid email address',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'google-domain',
|
||||
type: 'text',
|
||||
label: 'Domain',
|
||||
condition: {
|
||||
when: 'google-account-type',
|
||||
is: 'domain',
|
||||
},
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'google-email',
|
||||
'data-testid': 'input-google-email',
|
||||
type: 'text',
|
||||
label: 'Email address',
|
||||
condition: {
|
||||
or: [
|
||||
{ when: 'google-account-type', is: 'googleAccount' },
|
||||
{ when: 'google-account-type', is: 'serviceAccount' },
|
||||
{ when: 'google-account-type', is: 'googleGroup' },
|
||||
{ when: 'google-account-type', is: null },
|
||||
]
|
||||
},
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
type: validatorTypes.PATTERN,
|
||||
pattern: '[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,}$',
|
||||
message: 'Please enter a valid email address'
|
||||
}
|
||||
],
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'google-domain',
|
||||
type: 'text',
|
||||
label: 'Domain',
|
||||
condition: {
|
||||
when: 'google-account-type',
|
||||
is: 'domain'
|
||||
},
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,29 +4,33 @@ import validatorTypes from '@data-driven-forms/react-form-renderer/validator-typ
|
|||
import StepTemplate from './stepTemplate';
|
||||
|
||||
export default {
|
||||
StepTemplate,
|
||||
id: 'wizard-details',
|
||||
name: 'image-name',
|
||||
title: 'Name image',
|
||||
nextStep: 'review',
|
||||
fields: [
|
||||
StepTemplate,
|
||||
id: 'wizard-details',
|
||||
name: 'image-name',
|
||||
title: 'Name image',
|
||||
nextStep: 'review',
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'plain-text-component',
|
||||
label: (
|
||||
<p>
|
||||
Optionally enter a name for your image. All images will have a UUID.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'image-name',
|
||||
type: 'text',
|
||||
label: 'Image name',
|
||||
autoFocus: true,
|
||||
validate: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'plain-text-component',
|
||||
label: <p>Optionally enter a name for your image. All images will have a UUID.</p>
|
||||
type: validatorTypes.MAX_LENGTH,
|
||||
threshold: 100,
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'image-name',
|
||||
type: 'text',
|
||||
label: 'Image name',
|
||||
autoFocus: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.MAX_LENGTH,
|
||||
threshold: 100
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,42 +8,49 @@ import DocumentationButton from '../../sharedComponents/DocumentationButton';
|
|||
import StepTemplate from './stepTemplate';
|
||||
|
||||
export default {
|
||||
StepTemplate,
|
||||
id: 'wizard-imageoutput',
|
||||
title: 'Image output',
|
||||
name: 'image-output',
|
||||
nextStep: ({ values }) => nextStepMapper(values),
|
||||
fields: [
|
||||
StepTemplate,
|
||||
id: 'wizard-imageoutput',
|
||||
title: 'Image output',
|
||||
name: 'image-output',
|
||||
nextStep: ({ values }) => nextStepMapper(values),
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'image-output-plain-text',
|
||||
label: (
|
||||
<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: [
|
||||
{
|
||||
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>
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'output',
|
||||
name: 'target-environment',
|
||||
label: 'Select target environments',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
component: 'image-output-release-select',
|
||||
label: 'Release',
|
||||
name: 'release',
|
||||
initialValue: RHEL_9,
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED
|
||||
}
|
||||
],
|
||||
type: 'targetEnvironmentValidator',
|
||||
},
|
||||
{
|
||||
component: 'output',
|
||||
name: 'target-environment',
|
||||
label: 'Select target environments',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED
|
||||
},
|
||||
{
|
||||
type: 'targetEnvironmentValidator'
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import isRhel from '../../../Utilities/isRhel.js';
|
||||
|
||||
export default ({ 'target-environment': targetEnv, release } = {}, { skipAws, skipGoogle, skipAzure } = {}) => {
|
||||
if (!skipAws && targetEnv?.aws) {
|
||||
return 'aws-target-env';
|
||||
}
|
||||
export default (
|
||||
{ 'target-environment': targetEnv, release } = {},
|
||||
{ skipAws, skipGoogle, skipAzure } = {}
|
||||
) => {
|
||||
if (!skipAws && targetEnv?.aws) {
|
||||
return 'aws-target-env';
|
||||
}
|
||||
|
||||
if (!skipGoogle && targetEnv?.gcp) {
|
||||
return 'google-cloud-target-env';
|
||||
}
|
||||
if (!skipGoogle && targetEnv?.gcp) {
|
||||
return 'google-cloud-target-env';
|
||||
}
|
||||
|
||||
if (!skipAzure && targetEnv?.azure) {
|
||||
return 'ms-azure-target-env';
|
||||
}
|
||||
if (!skipAzure && targetEnv?.azure) {
|
||||
return 'ms-azure-target-env';
|
||||
}
|
||||
|
||||
return isRhel(release) ? 'registration' : 'File system configuration';
|
||||
return isRhel(release) ? 'registration' : 'File system configuration';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,103 +6,119 @@ import nextStepMapper from './imageOutputStepMapper';
|
|||
import StepTemplate from './stepTemplate';
|
||||
|
||||
export default {
|
||||
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 }),
|
||||
fields: [
|
||||
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,
|
||||
}),
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'azure-description',
|
||||
label: (
|
||||
<Text>
|
||||
Image Builder sends an image to an authorized Azure account.
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'azure-auth-expandable',
|
||||
name: 'azure-auth-expandable',
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'azure-destination',
|
||||
label: (
|
||||
<>
|
||||
<Title headingLevel="h2">Destination</Title>
|
||||
<Text>
|
||||
Your image will be uploaded to the resource group in the
|
||||
subscription you specify.
|
||||
</Text>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-tenant-id',
|
||||
className: 'pf-u-w-50',
|
||||
'data-testid': 'azure-tenant-id',
|
||||
type: 'text',
|
||||
label: 'Tenant ID',
|
||||
required: true,
|
||||
isRequired: true,
|
||||
autoFocus: true,
|
||||
validate: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'azure-description',
|
||||
label: <Text>
|
||||
Image Builder sends an image to an authorized Azure account.
|
||||
</Text>
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
component: 'azure-auth-expandable',
|
||||
name: 'azure-auth-expandable'
|
||||
type: validatorTypes.PATTERN,
|
||||
pattern:
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
message: 'Please enter a valid tenant ID',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'azure-auth-button',
|
||||
name: 'azure-auth-button',
|
||||
'data-testid': 'azure-auth-button',
|
||||
required: true,
|
||||
isRequired: true,
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-subscription-id',
|
||||
className: 'pf-u-w-50',
|
||||
'data-testid': 'azure-subscription-id',
|
||||
type: 'text',
|
||||
label: 'Subscription ID',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'azure-destination',
|
||||
label: <>
|
||||
<Title headingLevel="h2">Destination</Title>
|
||||
<Text>
|
||||
Your image will be uploaded to the resource group in the subscription you specify.
|
||||
</Text>
|
||||
</>
|
||||
|
||||
type: validatorTypes.PATTERN,
|
||||
pattern:
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
message: 'Please enter a valid subscription ID',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-resource-group',
|
||||
className: 'pf-u-w-50',
|
||||
'data-testid': 'azure-resource-group',
|
||||
type: 'text',
|
||||
label: 'Resource group',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-tenant-id',
|
||||
className: 'pf-u-w-50',
|
||||
'data-testid': 'azure-tenant-id',
|
||||
type: 'text',
|
||||
label: 'Tenant ID',
|
||||
required: true,
|
||||
isRequired: true,
|
||||
autoFocus: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
type: validatorTypes.PATTERN,
|
||||
pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
message: 'Please enter a valid tenant ID',
|
||||
}
|
||||
],
|
||||
type: validatorTypes.PATTERN,
|
||||
pattern: /^[-\w._()]+[-\w_()]$/,
|
||||
message:
|
||||
'Resource group names only allow alphanumeric characters, ' +
|
||||
'periods, underscores, hyphens, and parenthesis and cannot end in a period',
|
||||
},
|
||||
{
|
||||
component: 'azure-auth-button',
|
||||
name: 'azure-auth-button',
|
||||
'data-testid': 'azure-auth-button',
|
||||
required: true,
|
||||
isRequired: true,
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-subscription-id',
|
||||
className: 'pf-u-w-50',
|
||||
'data-testid': 'azure-subscription-id',
|
||||
type: 'text',
|
||||
label: 'Subscription ID',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
type: validatorTypes.PATTERN,
|
||||
pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
message: 'Please enter a valid subscription ID',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: componentTypes.TEXT_FIELD,
|
||||
name: 'azure-resource-group',
|
||||
className: 'pf-u-w-50',
|
||||
'data-testid': 'azure-resource-group',
|
||||
type: 'text',
|
||||
label: 'Resource group',
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
{
|
||||
type: validatorTypes.PATTERN,
|
||||
pattern: /^[-\w._()]+[-\w_()]$/,
|
||||
message: 'Resource group names only allow alphanumeric characters, ' +
|
||||
'periods, underscores, hyphens, and parenthesis and cannot end in a period',
|
||||
},
|
||||
],
|
||||
}
|
||||
// TODO check oauth2 thing too here?
|
||||
]
|
||||
],
|
||||
},
|
||||
// TODO check oauth2 thing too here?
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,22 +4,27 @@ import { Text } from '@patternfly/react-core';
|
|||
import StepTemplate from './stepTemplate';
|
||||
|
||||
export default {
|
||||
StepTemplate,
|
||||
id: 'wizard-systemconfiguration-packages',
|
||||
title: 'Packages',
|
||||
name: 'packages',
|
||||
substepOf: 'System Configuration',
|
||||
nextStep: 'image-name',
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'packages-text-component',
|
||||
label: <Text>Add optional additional packages to your image by searching available packages.</Text>
|
||||
},
|
||||
{
|
||||
component: 'package-selector',
|
||||
name: 'selected-packages',
|
||||
label: 'Available options'
|
||||
}
|
||||
]
|
||||
StepTemplate,
|
||||
id: 'wizard-systemconfiguration-packages',
|
||||
title: 'Packages',
|
||||
name: 'packages',
|
||||
substepOf: 'System Configuration',
|
||||
nextStep: 'image-name',
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'packages-text-component',
|
||||
label: (
|
||||
<Text>
|
||||
Add optional additional packages to your image by searching available
|
||||
packages.
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'package-selector',
|
||||
name: 'selected-packages',
|
||||
label: 'Available options',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,133 +1,142 @@
|
|||
import React from 'react';
|
||||
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
|
||||
import validatorTypes from '@data-driven-forms/react-form-renderer/validator-types';
|
||||
import { Button, Popover, Text, TextContent, TextVariants } from '@patternfly/react-core';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon, HelpIcon } from '@patternfly/react-icons';
|
||||
import StepTemplate from './stepTemplate';
|
||||
|
||||
const PopoverActivation = () => {
|
||||
return <Popover
|
||||
hasAutoWidth
|
||||
maxWidth='35rem'
|
||||
bodyContent={ <TextContent>
|
||||
<Text>
|
||||
Activation keys allow you to register a system with
|
||||
appropriate subscriptions and system purpose attached.
|
||||
</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>;
|
||||
return (
|
||||
<Popover
|
||||
hasAutoWidth
|
||||
maxWidth="35rem"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Activation keys allow you to register a system with appropriate
|
||||
subscriptions and system purpose attached.
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
StepTemplate,
|
||||
id: 'wizard-registration',
|
||||
title: 'Registration',
|
||||
name: 'registration',
|
||||
nextStep: 'File system configuration',
|
||||
fields: [
|
||||
StepTemplate,
|
||||
id: 'wizard-registration',
|
||||
title: 'Registration',
|
||||
name: 'registration',
|
||||
nextStep: 'File system configuration',
|
||||
fields: [
|
||||
{
|
||||
component: componentTypes.RADIO,
|
||||
label: 'Register images with Red Hat',
|
||||
name: 'register-system',
|
||||
initialValue: 'register-now-insights',
|
||||
options: [
|
||||
{
|
||||
component: componentTypes.RADIO,
|
||||
label: 'Register images with Red Hat',
|
||||
name: 'register-system',
|
||||
initialValue: 'register-now-insights',
|
||||
options: [
|
||||
{
|
||||
label: 'Register and connect image instances with Red Hat',
|
||||
description: 'Includes Subscriptions and Red Hat Insights',
|
||||
value: 'register-now-insights',
|
||||
'data-testid': 'radio-register-now-insights',
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
label: 'Register image instances only',
|
||||
description: 'Includes Subscriptions only',
|
||||
value: 'register-now',
|
||||
className: 'pf-u-mt-sm',
|
||||
'data-testid': 'radio-register-now',
|
||||
},
|
||||
{
|
||||
label: 'Register later',
|
||||
value: 'register-later',
|
||||
className: 'pf-u-mt-sm',
|
||||
'data-testid': 'radio-register-later',
|
||||
},
|
||||
]
|
||||
label: 'Register and connect image instances with Red Hat',
|
||||
description: 'Includes Subscriptions and Red Hat Insights',
|
||||
value: 'register-now-insights',
|
||||
'data-testid': 'radio-register-now-insights',
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
component: 'activation-keys',
|
||||
name: 'subscription-activation-key',
|
||||
required: true,
|
||||
label: (
|
||||
<>
|
||||
Activation key to use for this image
|
||||
<PopoverActivation />
|
||||
</>
|
||||
),
|
||||
condition: {
|
||||
or: [
|
||||
{ when: 'register-system', is: 'register-now-insights' },
|
||||
{ when: 'register-system', is: 'register-now' },
|
||||
]
|
||||
},
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
label: 'Register image instances only',
|
||||
description: 'Includes Subscriptions only',
|
||||
value: 'register-now',
|
||||
className: 'pf-u-mt-sm',
|
||||
'data-testid': 'radio-register-now',
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'subscription-activation-description',
|
||||
label: (
|
||||
<>
|
||||
Create and manage activation keys in the
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={ <ExternalLinkAltIcon /> }
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href="https://access.redhat.com/">
|
||||
Customer Portal
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
condition: {
|
||||
or: [
|
||||
{ when: 'register-system', is: 'register-now-insights' },
|
||||
{ when: 'register-system', is: 'register-now' },
|
||||
]
|
||||
},
|
||||
label: 'Register later',
|
||||
value: 'register-later',
|
||||
className: 'pf-u-mt-sm',
|
||||
'data-testid': 'radio-register-later',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'activation-keys',
|
||||
name: 'subscription-activation-key',
|
||||
required: true,
|
||||
label: (
|
||||
<>
|
||||
Activation key to use for this image
|
||||
<PopoverActivation />
|
||||
</>
|
||||
),
|
||||
condition: {
|
||||
or: [
|
||||
{ when: 'register-system', is: 'register-now-insights' },
|
||||
{ when: 'register-system', is: 'register-now' },
|
||||
],
|
||||
},
|
||||
isRequired: true,
|
||||
validate: [
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'subscription-register-later',
|
||||
label: (
|
||||
<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.
|
||||
</Text>
|
||||
<Text>
|
||||
Registering now is recommended.
|
||||
</Text>
|
||||
</TextContent>
|
||||
),
|
||||
condition: {
|
||||
or: [
|
||||
{ when: 'register-system', is: 'register-later' },
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
type: validatorTypes.REQUIRED,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: componentTypes.PLAIN_TEXT,
|
||||
name: 'subscription-activation-description',
|
||||
label: (
|
||||
<>
|
||||
Create and manage activation keys in the
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href="https://access.redhat.com/"
|
||||
>
|
||||
Customer Portal
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
condition: {
|
||||
or: [
|
||||
{ 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.
|
||||
</Text>
|
||||
<Text>Registering now is recommended.</Text>
|
||||
</TextContent>
|
||||
),
|
||||
condition: {
|
||||
or: [{ when: 'register-system', is: 'register-later' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ import CustomButtons from '../formComponents/CustomSubmitButtons';
|
|||
import StepTemplate from './stepTemplate';
|
||||
|
||||
export default {
|
||||
StepTemplate,
|
||||
id: 'wizard-review',
|
||||
name: 'review',
|
||||
title: 'Review',
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
name: 'review',
|
||||
component: 'review'
|
||||
}
|
||||
]
|
||||
StepTemplate,
|
||||
id: 'wizard-review',
|
||||
name: 'review',
|
||||
title: 'Review',
|
||||
buttons: CustomButtons,
|
||||
fields: [
|
||||
{
|
||||
name: 'review',
|
||||
component: 'review',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,31 +2,42 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Title } from '@patternfly/react-core';
|
||||
|
||||
const StepTemplate = ({ id, formFields, formRef, title, customTitle, showTitle, showTitles }) => (
|
||||
<div id={ id } ref={ formRef } className="pf-c-form">
|
||||
{((showTitles && showTitle !== false) || showTitle) &&
|
||||
(customTitle ? (
|
||||
customTitle
|
||||
) : (
|
||||
<Title headingLevel="h1" size="xl">
|
||||
{title}
|
||||
</Title>
|
||||
))}
|
||||
{formFields}
|
||||
</div>
|
||||
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) }) ]),
|
||||
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,30 +1,32 @@
|
|||
const FileSystemConfigurationValidator = () => fsc => {
|
||||
if (!fsc) {
|
||||
return undefined;
|
||||
}
|
||||
const FileSystemConfigurationValidator = () => (fsc) => {
|
||||
if (!fsc) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let mpFreqs = {};
|
||||
for (const fs of fsc) {
|
||||
const mp = fs.mountpoint;
|
||||
if (mp in mpFreqs) {
|
||||
mpFreqs[mp]++;
|
||||
} else {
|
||||
mpFreqs[mp] = 1;
|
||||
}
|
||||
let mpFreqs = {};
|
||||
for (const fs of fsc) {
|
||||
const mp = fs.mountpoint;
|
||||
if (mp in mpFreqs) {
|
||||
mpFreqs[mp]++;
|
||||
} else {
|
||||
mpFreqs[mp] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
let duplicates = [];
|
||||
for (const [ k, v ] of Object.entries(mpFreqs)) {
|
||||
if (v > 1) {
|
||||
duplicates.push(k);
|
||||
}
|
||||
let duplicates = [];
|
||||
for (const [k, v] of Object.entries(mpFreqs)) {
|
||||
if (v > 1) {
|
||||
duplicates.push(k);
|
||||
}
|
||||
}
|
||||
|
||||
let root = mpFreqs['/'] >= 1;
|
||||
return duplicates.length === 0 && root ? undefined : {
|
||||
let root = mpFreqs['/'] >= 1;
|
||||
return duplicates.length === 0 && root
|
||||
? undefined
|
||||
: {
|
||||
duplicates: duplicates === [] ? undefined : duplicates,
|
||||
root,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export default FileSystemConfigurationValidator;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
const TargetEnvironmentValidator = () => targets => {
|
||||
if (!targets) {
|
||||
return undefined;
|
||||
}
|
||||
const TargetEnvironmentValidator = () => (targets) => {
|
||||
if (!targets) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// at least one of the target environments must
|
||||
// be set to true. This reduces the value to
|
||||
// a single boolean which is a flag for whether
|
||||
// at least one target has been selected or not
|
||||
let valid = Object.values(targets).reduce((prev, curr) => curr || prev, false);
|
||||
return !valid ? 'Please select an image' : undefined;
|
||||
// at least one of the target environments must
|
||||
// be set to true. This reduces the value to
|
||||
// a single boolean which is a flag for whether
|
||||
// at least one target has been selected or not
|
||||
let valid = Object.values(targets).reduce(
|
||||
(prev, curr) => curr || prev,
|
||||
false
|
||||
);
|
||||
return !valid ? 'Please select an image' : undefined;
|
||||
};
|
||||
|
||||
export default TargetEnvironmentValidator;
|
||||
|
|
|
|||
|
|
@ -3,35 +3,35 @@ import PropTypes from 'prop-types';
|
|||
import { Alert } from '@patternfly/react-core';
|
||||
|
||||
const useGetErrorReason = (err) => {
|
||||
if (!err?.reason) {
|
||||
return 'An unknown error occured';
|
||||
}
|
||||
if (!err?.reason) {
|
||||
return 'An unknown error occured';
|
||||
}
|
||||
|
||||
if (err.details?.reason) {
|
||||
return err.details.reason;
|
||||
}
|
||||
if (err.details?.reason) {
|
||||
return err.details.reason;
|
||||
}
|
||||
|
||||
return err.reason;
|
||||
return err.reason;
|
||||
};
|
||||
|
||||
const ErrorDetails = ({ status }) => {
|
||||
if (!status || status.status !== 'failure') {
|
||||
return <></>;
|
||||
}
|
||||
if (!status || status.status !== 'failure') {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const reason = useGetErrorReason(status.error);
|
||||
const reason = useGetErrorReason(status.error);
|
||||
|
||||
return (
|
||||
<div className='pf-u-mt-sm'>
|
||||
<strong>Status</strong>
|
||||
<Alert variant="danger" title="Image build failed" isInline isPlain />
|
||||
<p className='pf-u-danger-color-200 pf-u-w-33-on-md'>{reason}</p>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="pf-u-mt-sm">
|
||||
<strong>Status</strong>
|
||||
<Alert variant="danger" title="Image build failed" isInline isPlain />
|
||||
<p className="pf-u-danger-color-200 pf-u-w-33-on-md">{reason}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ErrorDetails.propTypes = {
|
||||
status: PropTypes.object,
|
||||
status: PropTypes.object,
|
||||
};
|
||||
|
||||
export default ErrorDetails;
|
||||
|
|
|
|||
|
|
@ -2,72 +2,76 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Flex } from '@patternfly/react-core';
|
||||
import { CheckCircleIcon, PendingIcon, ExclamationCircleIcon, InProgressIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
PendingIcon,
|
||||
ExclamationCircleIcon,
|
||||
InProgressIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
import './ImageBuildStatus.scss';
|
||||
|
||||
const ImageBuildStatus = (props) => {
|
||||
const messages = {
|
||||
success: [
|
||||
{
|
||||
icon: <CheckCircleIcon className="success" />,
|
||||
text: 'Ready'
|
||||
}
|
||||
],
|
||||
failure: [
|
||||
{
|
||||
icon: <ExclamationCircleIcon className="error" />,
|
||||
text: 'Image build failed'
|
||||
}
|
||||
],
|
||||
pending: [
|
||||
{
|
||||
icon: <PendingIcon />,
|
||||
text: 'Image build is pending'
|
||||
}
|
||||
],
|
||||
// Keep "running" for backward compatibility
|
||||
running: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image build in progress'
|
||||
}
|
||||
],
|
||||
building: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image build in progress'
|
||||
}
|
||||
],
|
||||
uploading: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image upload in progress'
|
||||
}
|
||||
],
|
||||
registering: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Cloud registration in progress'
|
||||
}
|
||||
]
|
||||
};
|
||||
return (
|
||||
<React.Fragment>
|
||||
{messages[props.status] &&
|
||||
messages[props.status].map((message, key) => (
|
||||
<Flex key={ key } className="pf-u-align-items-baseline pf-m-nowrap">
|
||||
<div className="pf-u-mr-sm">{message.icon}</div>
|
||||
<small>{message.text}</small>
|
||||
</Flex>
|
||||
))
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
const messages = {
|
||||
success: [
|
||||
{
|
||||
icon: <CheckCircleIcon className="success" />,
|
||||
text: 'Ready',
|
||||
},
|
||||
],
|
||||
failure: [
|
||||
{
|
||||
icon: <ExclamationCircleIcon className="error" />,
|
||||
text: 'Image build failed',
|
||||
},
|
||||
],
|
||||
pending: [
|
||||
{
|
||||
icon: <PendingIcon />,
|
||||
text: 'Image build is pending',
|
||||
},
|
||||
],
|
||||
// Keep "running" for backward compatibility
|
||||
running: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image build in progress',
|
||||
},
|
||||
],
|
||||
building: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image build in progress',
|
||||
},
|
||||
],
|
||||
uploading: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image upload in progress',
|
||||
},
|
||||
],
|
||||
registering: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Cloud registration in progress',
|
||||
},
|
||||
],
|
||||
};
|
||||
return (
|
||||
<React.Fragment>
|
||||
{messages[props.status] &&
|
||||
messages[props.status].map((message, key) => (
|
||||
<Flex key={key} className="pf-u-align-items-baseline pf-m-nowrap">
|
||||
<div className="pf-u-mr-sm">{message.icon}</div>
|
||||
<small>{message.text}</small>
|
||||
</Flex>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
ImageBuildStatus.propTypes = {
|
||||
status: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImageBuildStatus;
|
||||
|
|
|
|||
|
|
@ -1,112 +1,129 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, TextContent, Text, TextVariants, Popover } from '@patternfly/react-core';
|
||||
import {
|
||||
Button,
|
||||
TextContent,
|
||||
Text,
|
||||
TextVariants,
|
||||
Popover,
|
||||
} from '@patternfly/react-core';
|
||||
import { DownloadIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
const ImageLink = (props) => {
|
||||
const fileExtensions = {
|
||||
vsphere: '.vmdk',
|
||||
'guest-image': '.qcow2',
|
||||
'image-installer': '.iso',
|
||||
};
|
||||
const fileExtensions = {
|
||||
vsphere: '.vmdk',
|
||||
'guest-image': '.qcow2',
|
||||
'image-installer': '.iso',
|
||||
};
|
||||
|
||||
const uploadStatus = props.imageStatus ? props.imageStatus.upload_status : undefined;
|
||||
if (uploadStatus) {
|
||||
if (uploadStatus.type === 'aws') {
|
||||
const url = 'https://console.aws.amazon.com/ec2/v2/home?region=' +
|
||||
uploadStatus.options.region +
|
||||
'#LaunchInstanceWizard:ami=' +
|
||||
uploadStatus.options.ami;
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={ <ExternalLinkAltIcon /> }
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={ url }>
|
||||
Launch instance
|
||||
</Button>
|
||||
);
|
||||
} else if (uploadStatus.type === 'azure') {
|
||||
const url = 'https://portal.azure.com/#@' + props.uploadOptions.tenant_id +
|
||||
'/resource/subscriptions/' + props.uploadOptions.subscription_id +
|
||||
'/resourceGroups/' + props.uploadOptions.resource_group +
|
||||
'/providers/Microsoft.Compute/images/' + uploadStatus.options.image_name;
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={ <ExternalLinkAltIcon /> }
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={ url }>
|
||||
View uploaded image
|
||||
</Button>
|
||||
);
|
||||
} else if (uploadStatus.type === 'gcp') {
|
||||
return (
|
||||
<Popover
|
||||
aria-label="Popover with google cloud platform image details"
|
||||
maxWidth='30rem'
|
||||
headerContent={ 'GCP image details' }
|
||||
bodyContent={ <TextContent>
|
||||
<Text component={ TextVariants.p }>
|
||||
To use an Image Builder created Google Cloud Platform (GCP) image in your project,
|
||||
specify the project ID and image name in your templates and configurations.
|
||||
</Text>
|
||||
<Text>
|
||||
<strong>Project ID</strong>
|
||||
<br />
|
||||
{uploadStatus.options.project_id}
|
||||
</Text>
|
||||
<Text>
|
||||
<strong>Image Name</strong>
|
||||
<br />
|
||||
{uploadStatus.options.image_name}
|
||||
</Text>
|
||||
<Text>
|
||||
<strong>Shared with</strong>
|
||||
<br />
|
||||
{/* the account the image is shared with is stored in the form type:account so this extracts the account */}
|
||||
{props.uploadOptions.share_with_accounts[0].split(':')[1]}
|
||||
</Text>
|
||||
</TextContent> }>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
isInline>
|
||||
Image details
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
} else if (uploadStatus.type === 'aws.s3') {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={ <DownloadIcon /> }
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={ uploadStatus.options.url }>
|
||||
Download {fileExtensions[props.imageType]}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
const uploadStatus = props.imageStatus
|
||||
? props.imageStatus.upload_status
|
||||
: undefined;
|
||||
if (uploadStatus) {
|
||||
if (uploadStatus.type === 'aws') {
|
||||
const url =
|
||||
'https://console.aws.amazon.com/ec2/v2/home?region=' +
|
||||
uploadStatus.options.region +
|
||||
'#LaunchInstanceWizard:ami=' +
|
||||
uploadStatus.options.ami;
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={url}
|
||||
>
|
||||
Launch instance
|
||||
</Button>
|
||||
);
|
||||
} else if (uploadStatus.type === 'azure') {
|
||||
const url =
|
||||
'https://portal.azure.com/#@' +
|
||||
props.uploadOptions.tenant_id +
|
||||
'/resource/subscriptions/' +
|
||||
props.uploadOptions.subscription_id +
|
||||
'/resourceGroups/' +
|
||||
props.uploadOptions.resource_group +
|
||||
'/providers/Microsoft.Compute/images/' +
|
||||
uploadStatus.options.image_name;
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={url}
|
||||
>
|
||||
View uploaded image
|
||||
</Button>
|
||||
);
|
||||
} else if (uploadStatus.type === 'gcp') {
|
||||
return (
|
||||
<Popover
|
||||
aria-label="Popover with google cloud platform image details"
|
||||
maxWidth="30rem"
|
||||
headerContent={'GCP image details'}
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text component={TextVariants.p}>
|
||||
To use an Image Builder created Google Cloud Platform (GCP)
|
||||
image in your project, specify the project ID and image name in
|
||||
your templates and configurations.
|
||||
</Text>
|
||||
<Text>
|
||||
<strong>Project ID</strong>
|
||||
<br />
|
||||
{uploadStatus.options.project_id}
|
||||
</Text>
|
||||
<Text>
|
||||
<strong>Image Name</strong>
|
||||
<br />
|
||||
{uploadStatus.options.image_name}
|
||||
</Text>
|
||||
<Text>
|
||||
<strong>Shared with</strong>
|
||||
<br />
|
||||
{/* the account the image is shared with is stored in the form type:account so this extracts the account */}
|
||||
{props.uploadOptions.share_with_accounts[0].split(':')[1]}
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button component="a" target="_blank" variant="link" isInline>
|
||||
Image details
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
} else if (uploadStatus.type === 'aws.s3') {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<DownloadIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={uploadStatus.options.url}
|
||||
>
|
||||
Download {fileExtensions[props.imageType]}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
};
|
||||
|
||||
ImageLink.propTypes = {
|
||||
imageStatus: PropTypes.object,
|
||||
imageType: PropTypes.string,
|
||||
uploadOptions: PropTypes.object,
|
||||
imageStatus: PropTypes.object,
|
||||
imageType: PropTypes.string,
|
||||
uploadOptions: PropTypes.object,
|
||||
};
|
||||
|
||||
export default ImageLink;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,28 @@ import PropTypes from 'prop-types';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { TableComposable, Thead, Tr, Th, Tbody, Td, ActionsColumn, ExpandableRowContent } from '@patternfly/react-table';
|
||||
import { EmptyState, EmptyStateVariant, EmptyStateIcon, EmptyStateBody, EmptyStateSecondaryActions,
|
||||
Pagination,
|
||||
Toolbar, ToolbarContent, ToolbarItem,
|
||||
Title } from '@patternfly/react-core';
|
||||
import {
|
||||
TableComposable,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Tbody,
|
||||
Td,
|
||||
ActionsColumn,
|
||||
ExpandableRowContent,
|
||||
} from '@patternfly/react-table';
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateVariant,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody,
|
||||
EmptyStateSecondaryActions,
|
||||
Pagination,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarItem,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import { PlusCircleIcon } from '@patternfly/react-icons';
|
||||
import './ImagesTable.scss';
|
||||
import { composesGet, composeGetStatus } from '../../store/actions/actions';
|
||||
|
|
@ -18,196 +35,255 @@ import ImageLink from './ImageLink';
|
|||
import ErrorDetails from './ImageBuildErrorDetails';
|
||||
|
||||
const ImagesTable = () => {
|
||||
const [ page, setPage ] = useState(1);
|
||||
const [ perPage, setPerPage ] = useState(10);
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
|
||||
const [ expandedComposeIds, setExpandedComposeIds ] = useState([]);
|
||||
const isExpanded = compose => expandedComposeIds.includes(compose.id);
|
||||
const [expandedComposeIds, setExpandedComposeIds] = useState([]);
|
||||
const isExpanded = (compose) => expandedComposeIds.includes(compose.id);
|
||||
|
||||
const handleToggle = (compose, isExpanding) => {
|
||||
if (isExpanding) {
|
||||
setExpandedComposeIds([ ...expandedComposeIds, compose.id ]);
|
||||
} else {
|
||||
setExpandedComposeIds(expandedComposeIds.filter(id => id !== compose.id));
|
||||
}};
|
||||
const handleToggle = (compose, isExpanding) => {
|
||||
if (isExpanding) {
|
||||
setExpandedComposeIds([...expandedComposeIds, compose.id]);
|
||||
} else {
|
||||
setExpandedComposeIds(
|
||||
expandedComposeIds.filter((id) => id !== compose.id)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const composes = useSelector((state) => state.composes);
|
||||
const dispatch = useDispatch();
|
||||
const composes = useSelector((state) => state.composes);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const pollComposeStatuses = () => {
|
||||
Object.entries(composes.byId).map(([ id, compose ]) => {
|
||||
/* Skip composes that have been complete */
|
||||
if (compose.image_status.status === 'success' || compose.image_status.status === 'failure') {
|
||||
return;
|
||||
}
|
||||
const pollComposeStatuses = () => {
|
||||
Object.entries(composes.byId).map(([id, compose]) => {
|
||||
/* Skip composes that have been complete */
|
||||
if (
|
||||
compose.image_status.status === 'success' ||
|
||||
compose.image_status.status === 'failure'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(composeGetStatus(id));
|
||||
});
|
||||
};
|
||||
dispatch(composeGetStatus(id));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(composesGet(perPage, 0));
|
||||
const intervalId = setInterval(() => pollComposeStatuses(), 8000);
|
||||
useEffect(() => {
|
||||
dispatch(composesGet(perPage, 0));
|
||||
const intervalId = setInterval(() => pollComposeStatuses(), 8000);
|
||||
|
||||
// clean up interval on unmount
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
// clean up interval on unmount
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const onSetPage = (_, page) => {
|
||||
// if the next page's composes haven't been fetched from api yet
|
||||
// then fetch them with proper page index and offset
|
||||
if (composes.count > composes.allIds.length) {
|
||||
const pageIndex = page - 1;
|
||||
const offset = pageIndex * perPage;
|
||||
dispatch(composesGet(perPage, offset));
|
||||
}
|
||||
const onSetPage = (_, page) => {
|
||||
// if the next page's composes haven't been fetched from api yet
|
||||
// then fetch them with proper page index and offset
|
||||
if (composes.count > composes.allIds.length) {
|
||||
const pageIndex = page - 1;
|
||||
const offset = pageIndex * perPage;
|
||||
dispatch(composesGet(perPage, offset));
|
||||
}
|
||||
|
||||
setPage(page);
|
||||
};
|
||||
setPage(page);
|
||||
};
|
||||
|
||||
const onPerPageSelect = (_, perPage) => {
|
||||
// if the new per page quantity is greater than the number of already fetched composes fetch more composes
|
||||
// if all composes haven't already been fetched
|
||||
if (composes.count > composes.allIds.length && perPage > composes.allIds.length) {
|
||||
dispatch(composesGet(perPage, 0));
|
||||
}
|
||||
const onPerPageSelect = (_, perPage) => {
|
||||
// if the new per page quantity is greater than the number of already fetched composes fetch more composes
|
||||
// if all composes haven't already been fetched
|
||||
if (
|
||||
composes.count > composes.allIds.length &&
|
||||
perPage > composes.allIds.length
|
||||
) {
|
||||
dispatch(composesGet(perPage, 0));
|
||||
}
|
||||
|
||||
// page should be reset to the first page when the page size is changed.
|
||||
setPerPage(perPage);
|
||||
setPage(1);
|
||||
};
|
||||
// page should be reset to the first page when the page size is changed.
|
||||
setPerPage(perPage);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const timestampToDisplayString = (ts) => {
|
||||
// timestamp has format 2021-04-27 12:31:12.794809 +0000 UTC
|
||||
// must be converted to ms timestamp and then reformatted to Apr 27, 2021
|
||||
if (!ts) {
|
||||
return '';
|
||||
}
|
||||
const timestampToDisplayString = (ts) => {
|
||||
// timestamp has format 2021-04-27 12:31:12.794809 +0000 UTC
|
||||
// must be converted to ms timestamp and then reformatted to Apr 27, 2021
|
||||
if (!ts) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// get YYYY-MM-DD format
|
||||
const date = ts.slice(0, 10);
|
||||
const ms = Date.parse(date);
|
||||
const options = { month: 'short', day: 'numeric', year: 'numeric' };
|
||||
const tsDisplay = new Intl.DateTimeFormat('en-US', options).format(ms);
|
||||
return tsDisplay;
|
||||
};
|
||||
// get YYYY-MM-DD format
|
||||
const date = ts.slice(0, 10);
|
||||
const ms = Date.parse(date);
|
||||
const options = { month: 'short', day: 'numeric', year: 'numeric' };
|
||||
const tsDisplay = new Intl.DateTimeFormat('en-US', options).format(ms);
|
||||
return tsDisplay;
|
||||
};
|
||||
|
||||
const actions = (compose) => [
|
||||
{
|
||||
title: 'Recreate image',
|
||||
onClick: () => navigate(
|
||||
'/imagewizard',
|
||||
{ state: { composeRequest: compose.request, initialStep: 'review' }}
|
||||
)
|
||||
}
|
||||
];
|
||||
const actions = (compose) => [
|
||||
{
|
||||
title: 'Recreate image',
|
||||
onClick: () =>
|
||||
navigate('/imagewizard', {
|
||||
state: { composeRequest: compose.request, initialStep: 'review' },
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
// the state.page is not an index so must be reduced by 1 get the starting index
|
||||
const itemsStartInclusive = (page - 1) * perPage;
|
||||
const itemsEndExclusive = itemsStartInclusive + perPage;
|
||||
// the state.page is not an index so must be reduced by 1 get the starting index
|
||||
const itemsStartInclusive = (page - 1) * perPage;
|
||||
const itemsEndExclusive = itemsStartInclusive + perPage;
|
||||
|
||||
return (
|
||||
return (
|
||||
<React.Fragment>
|
||||
{(composes.allIds.length === 0 && (
|
||||
<EmptyState variant={EmptyStateVariant.large} data-testid="empty-state">
|
||||
<EmptyStateIcon icon={PlusCircleIcon} />
|
||||
<Title headingLevel="h4" size="lg">
|
||||
Create an image
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
Create OS images for deployment in Amazon Web Services, Microsoft
|
||||
Azure and Google Cloud Platform. Images can include a custom package
|
||||
set and an activation key to automate the registration process.
|
||||
</EmptyStateBody>
|
||||
<Link
|
||||
to="/imagewizard"
|
||||
className="pf-c-button pf-m-primary"
|
||||
data-testid="create-image-action"
|
||||
>
|
||||
Create image
|
||||
</Link>
|
||||
<EmptyStateSecondaryActions>
|
||||
<DocumentationButton />
|
||||
</EmptyStateSecondaryActions>
|
||||
</EmptyState>
|
||||
)) || (
|
||||
<React.Fragment>
|
||||
{ composes.allIds.length === 0 && (
|
||||
<EmptyState variant={ EmptyStateVariant.large } data-testid="empty-state">
|
||||
<EmptyStateIcon icon={ PlusCircleIcon } />
|
||||
<Title headingLevel="h4" size="lg">
|
||||
Create an image
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
Create OS images for deployment in Amazon Web Services,
|
||||
Microsoft Azure and Google Cloud Platform. Images can
|
||||
include a custom package set and an activation key to
|
||||
automate the registration process.
|
||||
</EmptyStateBody>
|
||||
<Link to="/imagewizard" className="pf-c-button pf-m-primary" data-testid="create-image-action">
|
||||
Create image
|
||||
</Link>
|
||||
<EmptyStateSecondaryActions>
|
||||
<DocumentationButton />
|
||||
</EmptyStateSecondaryActions>
|
||||
</EmptyState>
|
||||
) || (
|
||||
<React.Fragment>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<Link to="/imagewizard" className="pf-c-button pf-m-primary" data-testid="create-image-action">
|
||||
Create image
|
||||
</Link>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem variant="pagination" align={ { default: 'alignRight' } }>
|
||||
<Pagination
|
||||
itemCount={ composes.count }
|
||||
perPage={ perPage }
|
||||
page={ page }
|
||||
onSetPage={ onSetPage }
|
||||
onPerPageSelect={ onPerPageSelect }
|
||||
widgetId="compose-pagination"
|
||||
data-testid="images-pagination"
|
||||
isCompact />
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<TableComposable variant="compact" data-testid="images-table">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th>Image name</Th>
|
||||
<Th>Created</Th>
|
||||
<Th>Release</Th>
|
||||
<Th>Target</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Instance</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
{ composes.allIds.slice(itemsStartInclusive, itemsEndExclusive).map((id, rowIndex) => {
|
||||
const compose = composes.byId[id];
|
||||
return (
|
||||
<Tbody key={ id } isExpanded={ isExpanded(compose) }>
|
||||
<Tr>
|
||||
<Td expand={ { rowIndex, isExpanded: isExpanded(compose),
|
||||
onToggle: () => handleToggle(compose, !isExpanded(compose)) } } />
|
||||
<Td dataLabel="Image name">{compose.request.image_name || id}</Td>
|
||||
<Td dataLabel="Created">{timestampToDisplayString(compose.created_at)}</Td>
|
||||
<Td dataLabel="Release"><Release release={ compose.request.distribution } /></Td>
|
||||
<Td dataLabel="Target"><Target
|
||||
uploadType={ compose.request.image_requests[0].upload_request.type }
|
||||
imageType={ compose.request.image_requests[0].image_type } /></Td>
|
||||
<Td dataLabel="Status"><ImageBuildStatus
|
||||
status={ compose.image_status ? compose.image_status.status : '' } /></Td>
|
||||
<Td dataLabel="Instance"><ImageLink
|
||||
imageStatus={ compose.image_status }
|
||||
imageType={ compose.request.image_requests[0].image_type }
|
||||
uploadOptions={ compose.request.image_requests[0].upload_request.options } /></Td>
|
||||
<Td><ActionsColumn items={ actions(compose) } /></Td>
|
||||
</Tr>
|
||||
<Tr isExpanded={ isExpanded(compose) }>
|
||||
<Td colSpan={ 8 }>
|
||||
<ExpandableRowContent>
|
||||
<strong>UUID</strong>
|
||||
<div>{ id }</div>
|
||||
<ErrorDetails status={ compose.image_status } />
|
||||
</ExpandableRowContent>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
);
|
||||
}) }
|
||||
</TableComposable>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<Link
|
||||
to="/imagewizard"
|
||||
className="pf-c-button pf-m-primary"
|
||||
data-testid="create-image-action"
|
||||
>
|
||||
Create image
|
||||
</Link>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem
|
||||
variant="pagination"
|
||||
align={{ default: 'alignRight' }}
|
||||
>
|
||||
<Pagination
|
||||
itemCount={composes.count}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={onSetPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
widgetId="compose-pagination"
|
||||
data-testid="images-pagination"
|
||||
isCompact
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<TableComposable variant="compact" data-testid="images-table">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th>Image name</Th>
|
||||
<Th>Created</Th>
|
||||
<Th>Release</Th>
|
||||
<Th>Target</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Instance</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
{composes.allIds
|
||||
.slice(itemsStartInclusive, itemsEndExclusive)
|
||||
.map((id, rowIndex) => {
|
||||
const compose = composes.byId[id];
|
||||
return (
|
||||
<Tbody key={id} isExpanded={isExpanded(compose)}>
|
||||
<Tr>
|
||||
<Td
|
||||
expand={{
|
||||
rowIndex,
|
||||
isExpanded: isExpanded(compose),
|
||||
onToggle: () =>
|
||||
handleToggle(compose, !isExpanded(compose)),
|
||||
}}
|
||||
/>
|
||||
<Td dataLabel="Image name">
|
||||
{compose.request.image_name || id}
|
||||
</Td>
|
||||
<Td dataLabel="Created">
|
||||
{timestampToDisplayString(compose.created_at)}
|
||||
</Td>
|
||||
<Td dataLabel="Release">
|
||||
<Release release={compose.request.distribution} />
|
||||
</Td>
|
||||
<Td dataLabel="Target">
|
||||
<Target
|
||||
uploadType={
|
||||
compose.request.image_requests[0].upload_request
|
||||
.type
|
||||
}
|
||||
imageType={
|
||||
compose.request.image_requests[0].image_type
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
<Td dataLabel="Status">
|
||||
<ImageBuildStatus
|
||||
status={
|
||||
compose.image_status
|
||||
? compose.image_status.status
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
<Td dataLabel="Instance">
|
||||
<ImageLink
|
||||
imageStatus={compose.image_status}
|
||||
imageType={
|
||||
compose.request.image_requests[0].image_type
|
||||
}
|
||||
uploadOptions={
|
||||
compose.request.image_requests[0].upload_request
|
||||
.options
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<ActionsColumn items={actions(compose)} />
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr isExpanded={isExpanded(compose)}>
|
||||
<Td colSpan={8}>
|
||||
<ExpandableRowContent>
|
||||
<strong>UUID</strong>
|
||||
<div>{id}</div>
|
||||
<ErrorDetails status={compose.image_status} />
|
||||
</ExpandableRowContent>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
);
|
||||
})}
|
||||
</TableComposable>
|
||||
</React.Fragment>
|
||||
);
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
ImagesTable.propTypes = {
|
||||
composes: PropTypes.object,
|
||||
composesGet: PropTypes.func,
|
||||
composeGetStatus: PropTypes.func,
|
||||
composes: PropTypes.object,
|
||||
composesGet: PropTypes.func,
|
||||
composeGetStatus: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ImagesTable;
|
||||
|
|
|
|||
|
|
@ -5,18 +5,20 @@ import { Label } from '@patternfly/react-core';
|
|||
import { RHEL_8, RHEL_9 } from '../../constants.js';
|
||||
|
||||
const Release = (props) => {
|
||||
const releaseOptions = {
|
||||
[RHEL_8]: 'RHEL 8',
|
||||
[RHEL_9]: 'RHEL 9',
|
||||
'centos-8': 'CentOS Stream 8',
|
||||
'centos-9': 'CentOS Stream 9',
|
||||
};
|
||||
const release = releaseOptions[props.release] ? releaseOptions[props.release] : props.release;
|
||||
return <Label color='blue'>{release}</Label>;
|
||||
const releaseOptions = {
|
||||
[RHEL_8]: 'RHEL 8',
|
||||
[RHEL_9]: 'RHEL 9',
|
||||
'centos-8': 'CentOS Stream 8',
|
||||
'centos-9': 'CentOS Stream 9',
|
||||
};
|
||||
const release = releaseOptions[props.release]
|
||||
? releaseOptions[props.release]
|
||||
: props.release;
|
||||
return <Label color="blue">{release}</Label>;
|
||||
};
|
||||
|
||||
Release.propTypes = {
|
||||
release: PropTypes.string,
|
||||
release: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Release;
|
||||
|
|
|
|||
|
|
@ -2,32 +2,28 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
const Target = (props) => {
|
||||
const targetOptions = {
|
||||
aws: 'Amazon Web Services',
|
||||
azure: 'Microsoft Azure',
|
||||
gcp: 'Google Cloud Platform',
|
||||
vsphere: 'VMWare',
|
||||
'guest-image': 'Virtualization - Guest image',
|
||||
'image-installer': 'Bare metal - Installer'
|
||||
};
|
||||
const targetOptions = {
|
||||
aws: 'Amazon Web Services',
|
||||
azure: 'Microsoft Azure',
|
||||
gcp: 'Google Cloud Platform',
|
||||
vsphere: 'VMWare',
|
||||
'guest-image': 'Virtualization - Guest image',
|
||||
'image-installer': 'Bare metal - Installer',
|
||||
};
|
||||
|
||||
let target;
|
||||
if (props.uploadType === 'aws.s3') {
|
||||
target = targetOptions[props.imageType];
|
||||
} else {
|
||||
target = targetOptions[props.uploadType];
|
||||
}
|
||||
let target;
|
||||
if (props.uploadType === 'aws.s3') {
|
||||
target = targetOptions[props.imageType];
|
||||
} else {
|
||||
target = targetOptions[props.uploadType];
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{target}
|
||||
</>
|
||||
);
|
||||
return <>{target}</>;
|
||||
};
|
||||
|
||||
Target.propTypes = {
|
||||
uploadType: PropTypes.string,
|
||||
imageType: PropTypes.string
|
||||
uploadType: PropTypes.string,
|
||||
imageType: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Target;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { PageHeader, PageHeaderTitle } from '@redhat-cloud-services/frontend-components';
|
||||
// eslint-disable-next-line rulesdir/disallow-fec-relative-imports
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderTitle,
|
||||
} from '@redhat-cloud-services/frontend-components';
|
||||
|
||||
import { Button, Popover, TextContent, Text } from '@patternfly/react-core';
|
||||
import { GithubIcon, HelpIcon } from '@patternfly/react-icons';
|
||||
|
|
@ -12,49 +16,57 @@ import './LandingPage.scss';
|
|||
import DocumentationButton from '../sharedComponents/DocumentationButton';
|
||||
|
||||
class LandingPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader>
|
||||
<PageHeaderTitle className="title" title="Image Builder" />
|
||||
<Popover
|
||||
headerContent={ 'About Image Builder' }
|
||||
bodyContent={ <TextContent>
|
||||
<Text>
|
||||
Image Builder is a service that allows you to create RHEL images
|
||||
and push them to cloud environments.
|
||||
</Text>
|
||||
<DocumentationButton />
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={ <GithubIcon /> }
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={ 'https://github.com/RedHatInsights/image-builder-frontend/tree/' + COMMITHASH }>
|
||||
Contribute on GitHub
|
||||
</Button>
|
||||
</TextContent> }>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="About image builder"
|
||||
className="pf-u-pl-sm">
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</PageHeader>
|
||||
<section className="pf-l-page__main-section pf-c-page__main-section">
|
||||
<ImagesTable />
|
||||
</section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader>
|
||||
<PageHeaderTitle className="title" title="Image Builder" />
|
||||
<Popover
|
||||
headerContent={'About Image Builder'}
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Image Builder is a service that allows you to create RHEL
|
||||
images and push them to cloud environments.
|
||||
</Text>
|
||||
<DocumentationButton />
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<GithubIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={
|
||||
'https://github.com/RedHatInsights/image-builder-frontend/tree/' +
|
||||
COMMITHASH
|
||||
}
|
||||
>
|
||||
Contribute on GitHub
|
||||
</Button>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="About image builder"
|
||||
className="pf-u-pl-sm"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</PageHeader>
|
||||
<section className="pf-l-page__main-section pf-c-page__main-section">
|
||||
<ImagesTable />
|
||||
</section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LandingPage;
|
||||
|
|
|
|||
|
|
@ -3,19 +3,22 @@ import { Button } from '@patternfly/react-core';
|
|||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
const DocumentationButton = () => {
|
||||
const documentationURL =
|
||||
'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/uploading_a_customized_rhel_system_image_to_cloud_environments/index';
|
||||
const documentationURL =
|
||||
'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/uploading_a_customized_rhel_system_image_to_cloud_environments/index';
|
||||
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={ <ExternalLinkAltIcon /> }
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={ documentationURL }>Documentation</Button>
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={documentationURL}
|
||||
>
|
||||
Documentation
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentationButton;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@ import React, { lazy } from 'react';
|
|||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
const LandingPage = lazy(() => import('./Components/LandingPage/LandingPage'));
|
||||
const CreateImageWizard = lazy(() => import('./Components/CreateImageWizard/CreateImageWizard'));
|
||||
const CreateImageWizard = lazy(() =>
|
||||
import('./Components/CreateImageWizard/CreateImageWizard')
|
||||
);
|
||||
|
||||
export const Router = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='/imagewizard/*' element={ <CreateImageWizard /> } />
|
||||
<Route path='*' element={ <LandingPage /> } />
|
||||
</Routes>
|
||||
);
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/imagewizard/*" element={<CreateImageWizard />} />
|
||||
<Route path="*" element={<LandingPage />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
function getBaseName(pathname) {
|
||||
let release = '/';
|
||||
const pathName = pathname.split('/');
|
||||
let release = '/';
|
||||
const pathName = pathname.split('/');
|
||||
|
||||
pathName.shift();
|
||||
|
||||
if (pathName[0] === 'beta') {
|
||||
pathName.shift();
|
||||
release = `/beta/`;
|
||||
}
|
||||
|
||||
if (pathName[0] === 'beta') {
|
||||
pathName.shift();
|
||||
release = `/beta/`;
|
||||
}
|
||||
|
||||
return `${release}${pathName[0]}/${pathName[1] || ''}`;
|
||||
return `${release}${pathName[0]}/${pathName[1] || ''}`;
|
||||
}
|
||||
|
||||
export default getBaseName;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
import {
|
||||
RHEL_8,
|
||||
RHEL_9,
|
||||
} from '../constants';
|
||||
import { RHEL_8, RHEL_9 } from '../constants';
|
||||
|
||||
function isRhel(distro) {
|
||||
switch (distro) {
|
||||
case RHEL_8:
|
||||
case RHEL_9:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
switch (distro) {
|
||||
case RHEL_8:
|
||||
case RHEL_9:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default isRhel;
|
||||
|
|
|
|||
79
src/api.js
79
src/api.js
|
|
@ -1,62 +1,63 @@
|
|||
import axios from 'axios';
|
||||
import {
|
||||
IMAGE_BUILDER_API,
|
||||
RHSM_API,
|
||||
} from './constants';
|
||||
import { IMAGE_BUILDER_API, RHSM_API } from './constants';
|
||||
|
||||
const postHeaders = { headers: { 'Content-Type': 'application/json' }};
|
||||
const postHeaders = { headers: { 'Content-Type': 'application/json' } };
|
||||
|
||||
async function composeImage(body) {
|
||||
let path = '/compose';
|
||||
const request = await axios.post(IMAGE_BUILDER_API.concat(path), body, postHeaders);
|
||||
return request.data;
|
||||
let path = '/compose';
|
||||
const request = await axios.post(
|
||||
IMAGE_BUILDER_API.concat(path),
|
||||
body,
|
||||
postHeaders
|
||||
);
|
||||
return request.data;
|
||||
}
|
||||
|
||||
async function getComposes(limit, offset) {
|
||||
const params = new URLSearchParams({
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
let path = '/composes?' + params.toString();
|
||||
const request = await axios.get(IMAGE_BUILDER_API.concat(path));
|
||||
return request.data;
|
||||
const params = new URLSearchParams({
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
let path = '/composes?' + params.toString();
|
||||
const request = await axios.get(IMAGE_BUILDER_API.concat(path));
|
||||
return request.data;
|
||||
}
|
||||
|
||||
async function getComposeStatus(id) {
|
||||
let path = '/composes/' + id;
|
||||
const request = await axios.get(IMAGE_BUILDER_API.concat(path));
|
||||
return request.data;
|
||||
let path = '/composes/' + id;
|
||||
const request = await axios.get(IMAGE_BUILDER_API.concat(path));
|
||||
return request.data;
|
||||
}
|
||||
|
||||
async function getPackages(distribution, architecture, search, limit) {
|
||||
const params = new URLSearchParams({
|
||||
distribution,
|
||||
architecture,
|
||||
search,
|
||||
});
|
||||
limit && params.append('limit', limit);
|
||||
let path = '/packages?' + params.toString();
|
||||
const request = await axios.get(IMAGE_BUILDER_API.concat(path));
|
||||
return request.data;
|
||||
const params = new URLSearchParams({
|
||||
distribution,
|
||||
architecture,
|
||||
search,
|
||||
});
|
||||
limit && params.append('limit', limit);
|
||||
let path = '/packages?' + params.toString();
|
||||
const request = await axios.get(IMAGE_BUILDER_API.concat(path));
|
||||
return request.data;
|
||||
}
|
||||
|
||||
async function getVersion() {
|
||||
let path = '/version';
|
||||
const request = await axios.get(IMAGE_BUILDER_API.concat(path));
|
||||
return request.data;
|
||||
let path = '/version';
|
||||
const request = await axios.get(IMAGE_BUILDER_API.concat(path));
|
||||
return request.data;
|
||||
}
|
||||
|
||||
async function getActivationKeys() {
|
||||
const path = '/activation_keys';
|
||||
const request = await axios.get(RHSM_API.concat(path));
|
||||
return request.data.body;
|
||||
const path = '/activation_keys';
|
||||
const request = await axios.get(RHSM_API.concat(path));
|
||||
return request.data.body;
|
||||
}
|
||||
|
||||
export default {
|
||||
composeImage,
|
||||
getComposes,
|
||||
getComposeStatus,
|
||||
getPackages,
|
||||
getVersion,
|
||||
getActivationKeys,
|
||||
composeImage,
|
||||
getComposes,
|
||||
getComposeStatus,
|
||||
getPackages,
|
||||
getVersion,
|
||||
getActivationKeys,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ export const UNIT_MIB = 1024 ** 2;
|
|||
export const UNIT_GIB = 1024 ** 3;
|
||||
|
||||
export const RELEASES = {
|
||||
[RHEL_8]: 'Red Hat Enterprise Linux (RHEL) 8',
|
||||
[RHEL_9]: 'Red Hat Enterprise Linux (RHEL) 9',
|
||||
'centos-8': 'CentOS Stream 8',
|
||||
'centos-9': 'CentOS Stream 9',
|
||||
[RHEL_8]: 'Red Hat Enterprise Linux (RHEL) 8',
|
||||
[RHEL_9]: 'Red Hat Enterprise Linux (RHEL) 9',
|
||||
'centos-8': 'CentOS Stream 8',
|
||||
'centos-9': 'CentOS Stream 9',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import getBaseName from './Utilities/getBaseName';
|
||||
|
||||
describe('Utilities/getBaseName', () => {
|
||||
it('should find the right base name on Stable ', () => {
|
||||
expect(getBaseName('/insights/foo/bar/baz')).toEqual('/insights/foo');
|
||||
expect(getBaseName('/rhcs/bar/bar/baz')).toEqual('/rhcs/bar');
|
||||
});
|
||||
it('should find the right base name on Stable ', () => {
|
||||
expect(getBaseName('/insights/foo/bar/baz')).toEqual('/insights/foo');
|
||||
expect(getBaseName('/rhcs/bar/bar/baz')).toEqual('/rhcs/bar');
|
||||
});
|
||||
|
||||
it('should find the right base name on Beta ', () => {
|
||||
expect(getBaseName('/beta/insights/foo/bar/baz')).toEqual('/beta/insights/foo');
|
||||
expect(getBaseName('/beta/test/fff/bar/baz')).toEqual('/beta/test/fff');
|
||||
});
|
||||
it('should find the right base name on Beta ', () => {
|
||||
expect(getBaseName('/beta/insights/foo/bar/baz')).toEqual(
|
||||
'/beta/insights/foo'
|
||||
);
|
||||
expect(getBaseName('/beta/test/fff/bar/baz')).toEqual('/beta/test/fff');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,150 +2,157 @@ import api from '../../api';
|
|||
import types from '../types';
|
||||
|
||||
function composeUpdated(compose) {
|
||||
return {
|
||||
type: types.COMPOSE_UPDATED,
|
||||
payload: { compose },
|
||||
};
|
||||
return {
|
||||
type: types.COMPOSE_UPDATED,
|
||||
payload: { compose },
|
||||
};
|
||||
}
|
||||
|
||||
export const composeFailed = (error) => ({
|
||||
type: types.COMPOSE_FAILED,
|
||||
payload: { error }
|
||||
type: types.COMPOSE_FAILED,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
export const composeAdded = (compose, insert) => ({
|
||||
type: types.COMPOSE_ADDED,
|
||||
payload: { compose, insert },
|
||||
type: types.COMPOSE_ADDED,
|
||||
payload: { compose, insert },
|
||||
});
|
||||
|
||||
export const composeStart = (composeRequest) => async dispatch => {
|
||||
// response will be of the format {id: ''}
|
||||
const request = api.composeImage(composeRequest);
|
||||
return request.then(response => {
|
||||
// add the compose id to the compose object to provide access to the id if iterating through
|
||||
// composes and add an image status of 'pending' alongside the compose request.
|
||||
const compose = Object.assign({}, response, { request: composeRequest }, { image_status: { status: 'pending' }});
|
||||
dispatch(composeAdded(compose, true));
|
||||
}).catch(err => {
|
||||
if (err.response.status === 500) {
|
||||
dispatch(composeFailed('Error: Something went wrong serverside'));
|
||||
} else {
|
||||
dispatch(composeFailed('Error: Something went wrong with the compose'));
|
||||
}
|
||||
export const composeStart = (composeRequest) => async (dispatch) => {
|
||||
// response will be of the format {id: ''}
|
||||
const request = api.composeImage(composeRequest);
|
||||
return request
|
||||
.then((response) => {
|
||||
// add the compose id to the compose object to provide access to the id if iterating through
|
||||
// composes and add an image status of 'pending' alongside the compose request.
|
||||
const compose = Object.assign(
|
||||
{},
|
||||
response,
|
||||
{ request: composeRequest },
|
||||
{ image_status: { status: 'pending' } }
|
||||
);
|
||||
dispatch(composeAdded(compose, true));
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.status === 500) {
|
||||
dispatch(composeFailed('Error: Something went wrong serverside'));
|
||||
} else {
|
||||
dispatch(composeFailed('Error: Something went wrong with the compose'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const composeUpdatedStatus = (id, status) => ({
|
||||
type: types.COMPOSE_UPDATED_STATUS,
|
||||
payload: { id, status }
|
||||
type: types.COMPOSE_UPDATED_STATUS,
|
||||
payload: { id, status },
|
||||
});
|
||||
|
||||
export const composeGetStatus = (id) => async dispatch => {
|
||||
const request = await api.getComposeStatus(id);
|
||||
dispatch(composeUpdatedStatus(id, request.image_status));
|
||||
export const composeGetStatus = (id) => async (dispatch) => {
|
||||
const request = await api.getComposeStatus(id);
|
||||
dispatch(composeUpdatedStatus(id, request.image_status));
|
||||
};
|
||||
|
||||
export const composesUpdatedCount = (count) => ({
|
||||
type: types.COMPOSES_UPDATED_COUNT,
|
||||
payload: { count }
|
||||
type: types.COMPOSES_UPDATED_COUNT,
|
||||
payload: { count },
|
||||
});
|
||||
|
||||
export const composesGet = (limit, offset) => async dispatch => {
|
||||
const request = await api.getComposes(limit, offset);
|
||||
request.data.map(compose => {
|
||||
dispatch(composeAdded(compose, false));
|
||||
dispatch(composeGetStatus(compose.id));
|
||||
});
|
||||
dispatch(composesUpdatedCount(request.meta.count));
|
||||
export const composesGet = (limit, offset) => async (dispatch) => {
|
||||
const request = await api.getComposes(limit, offset);
|
||||
request.data.map((compose) => {
|
||||
dispatch(composeAdded(compose, false));
|
||||
dispatch(composeGetStatus(compose.id));
|
||||
});
|
||||
dispatch(composesUpdatedCount(request.meta.count));
|
||||
};
|
||||
|
||||
function setRelease({ arch, distro }) {
|
||||
return {
|
||||
type: types.SET_RELEASE,
|
||||
payload: {
|
||||
arch,
|
||||
distro,
|
||||
}
|
||||
};
|
||||
return {
|
||||
type: types.SET_RELEASE,
|
||||
payload: {
|
||||
arch,
|
||||
distro,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setUploadDestinations({ aws, azure, google }) {
|
||||
return {
|
||||
type: types.SET_UPLOAD_DESTINATIONS,
|
||||
payload: {
|
||||
aws,
|
||||
azure,
|
||||
google,
|
||||
}
|
||||
};
|
||||
return {
|
||||
type: types.SET_UPLOAD_DESTINATIONS,
|
||||
payload: {
|
||||
aws,
|
||||
azure,
|
||||
google,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setUploadAWS({ shareWithAccounts }) {
|
||||
return {
|
||||
type: types.SET_UPLOAD_AWS,
|
||||
payload: {
|
||||
shareWithAccounts,
|
||||
}
|
||||
};
|
||||
return {
|
||||
type: types.SET_UPLOAD_AWS,
|
||||
payload: {
|
||||
shareWithAccounts,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setUploadAzure({ tenantId, subscriptionId, resourceGroup }) {
|
||||
return {
|
||||
type: types.SET_UPLOAD_AZURE,
|
||||
payload: {
|
||||
tenantId,
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
}
|
||||
};
|
||||
return {
|
||||
type: types.SET_UPLOAD_AZURE,
|
||||
payload: {
|
||||
tenantId,
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setUploadGoogle({ accountType, shareWithAccounts }) {
|
||||
return {
|
||||
type: types.SET_UPLOAD_GOOGLE,
|
||||
payload: {
|
||||
accountType,
|
||||
shareWithAccounts,
|
||||
}
|
||||
};
|
||||
return {
|
||||
type: types.SET_UPLOAD_GOOGLE,
|
||||
payload: {
|
||||
accountType,
|
||||
shareWithAccounts,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setSelectedPackages(selectedPackages) {
|
||||
return {
|
||||
type: types.SET_SELECTED_PACKAGES,
|
||||
payload: selectedPackages
|
||||
};
|
||||
return {
|
||||
type: types.SET_SELECTED_PACKAGES,
|
||||
payload: selectedPackages,
|
||||
};
|
||||
}
|
||||
|
||||
function setSubscription({ activationKey, insights, organization }) {
|
||||
return {
|
||||
type: types.SET_SUBSCRIPTION,
|
||||
payload: {
|
||||
activationKey,
|
||||
insights,
|
||||
organization,
|
||||
}
|
||||
};
|
||||
return {
|
||||
type: types.SET_SUBSCRIPTION,
|
||||
payload: {
|
||||
activationKey,
|
||||
insights,
|
||||
organization,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setSubscribeNow(subscribeNow) {
|
||||
return {
|
||||
type: types.SET_SUBSCRIBE_NOW,
|
||||
payload: subscribeNow
|
||||
};
|
||||
return {
|
||||
type: types.SET_SUBSCRIBE_NOW,
|
||||
payload: subscribeNow,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
composesGet,
|
||||
composeStart,
|
||||
composeUpdated,
|
||||
composeGetStatus,
|
||||
setRelease,
|
||||
setUploadDestinations,
|
||||
setUploadAWS,
|
||||
setUploadAzure,
|
||||
setUploadGoogle,
|
||||
setSelectedPackages,
|
||||
setSubscription,
|
||||
setSubscribeNow,
|
||||
composesGet,
|
||||
composeStart,
|
||||
composeUpdated,
|
||||
composeGetStatus,
|
||||
setRelease,
|
||||
setUploadDestinations,
|
||||
setUploadAWS,
|
||||
setUploadAzure,
|
||||
setUploadGoogle,
|
||||
setSelectedPackages,
|
||||
setSubscription,
|
||||
setSubscribeNow,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,32 +7,32 @@ import composes from './reducers/composes';
|
|||
|
||||
let registry;
|
||||
|
||||
export function init (store = {}, ...middleware) {
|
||||
if (!registry) {
|
||||
registry = new ReducerRegistry(store, [
|
||||
promiseMiddleware,
|
||||
thunk,
|
||||
...middleware.filter(item => typeof item !== 'undefined'),
|
||||
]);
|
||||
export function init(store = {}, ...middleware) {
|
||||
if (!registry) {
|
||||
registry = new ReducerRegistry(store, [
|
||||
promiseMiddleware,
|
||||
thunk,
|
||||
...middleware.filter((item) => typeof item !== 'undefined'),
|
||||
]);
|
||||
|
||||
registry.register({
|
||||
composes,
|
||||
notifications: notificationsReducer,
|
||||
});
|
||||
}
|
||||
registry.register({
|
||||
composes,
|
||||
notifications: notificationsReducer,
|
||||
});
|
||||
}
|
||||
|
||||
return registry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
export function getStore () {
|
||||
return registry.getStore();
|
||||
export function getStore() {
|
||||
return registry.getStore();
|
||||
}
|
||||
|
||||
export function register (...args) {
|
||||
return registry.register(...args);
|
||||
export function register(...args) {
|
||||
return registry.register(...args);
|
||||
}
|
||||
|
||||
/* added for testing purposes only */
|
||||
export function clearStore() {
|
||||
registry = undefined;
|
||||
registry = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,74 +25,78 @@ import types from '../types';
|
|||
// };
|
||||
|
||||
const initialComposesState = {
|
||||
count: 0,
|
||||
allIds: [],
|
||||
byId: {},
|
||||
error: null,
|
||||
count: 0,
|
||||
allIds: [],
|
||||
byId: {},
|
||||
error: null,
|
||||
};
|
||||
|
||||
// only add to array if compose does not exist
|
||||
const updateAllIds = (allIds, id, insert) => {
|
||||
if (allIds.includes(id)) {
|
||||
return allIds;
|
||||
}
|
||||
if (allIds.includes(id)) {
|
||||
return allIds;
|
||||
}
|
||||
|
||||
if (insert) {
|
||||
return [ id ].concat(allIds);
|
||||
}
|
||||
if (insert) {
|
||||
return [id].concat(allIds);
|
||||
}
|
||||
|
||||
return allIds.concat(id);
|
||||
return allIds.concat(id);
|
||||
};
|
||||
|
||||
export function composes(state = initialComposesState, action) {
|
||||
switch (action.type) {
|
||||
case types.COMPOSE_ADDED:
|
||||
return {
|
||||
...state,
|
||||
allIds: updateAllIds(state.allIds, action.payload.compose.id, action.payload.insert),
|
||||
byId: {
|
||||
...state.byId,
|
||||
[action.payload.compose.id]: action.payload.compose,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
case types.COMPOSE_FAILED:
|
||||
return {
|
||||
...state,
|
||||
error: action.payload.error,
|
||||
};
|
||||
case types.COMPOSE_PENDING:
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
};
|
||||
case types.COMPOSE_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
byId: {
|
||||
...state.byId,
|
||||
[action.payload.compose.id]: action.payload.compose,
|
||||
}
|
||||
};
|
||||
case types.COMPOSES_UPDATED_COUNT:
|
||||
return {
|
||||
...state,
|
||||
count: action.payload.count,
|
||||
};
|
||||
case types.COMPOSE_UPDATED_STATUS:
|
||||
return {
|
||||
...state,
|
||||
byId: {
|
||||
...state.byId,
|
||||
[action.payload.id]: {
|
||||
...state.byId[action.payload.id],
|
||||
image_status: action.payload.status,
|
||||
}
|
||||
}
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case types.COMPOSE_ADDED:
|
||||
return {
|
||||
...state,
|
||||
allIds: updateAllIds(
|
||||
state.allIds,
|
||||
action.payload.compose.id,
|
||||
action.payload.insert
|
||||
),
|
||||
byId: {
|
||||
...state.byId,
|
||||
[action.payload.compose.id]: action.payload.compose,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
case types.COMPOSE_FAILED:
|
||||
return {
|
||||
...state,
|
||||
error: action.payload.error,
|
||||
};
|
||||
case types.COMPOSE_PENDING:
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
};
|
||||
case types.COMPOSE_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
byId: {
|
||||
...state.byId,
|
||||
[action.payload.compose.id]: action.payload.compose,
|
||||
},
|
||||
};
|
||||
case types.COMPOSES_UPDATED_COUNT:
|
||||
return {
|
||||
...state,
|
||||
count: action.payload.count,
|
||||
};
|
||||
case types.COMPOSE_UPDATED_STATUS:
|
||||
return {
|
||||
...state,
|
||||
byId: {
|
||||
...state.byId,
|
||||
[action.payload.id]: {
|
||||
...state.byId[action.payload.id],
|
||||
image_status: action.payload.status,
|
||||
},
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default composes;
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ const SET_SUBSCRIPTION = 'SET_SUBSCRIPTION';
|
|||
const SET_SUBSCRIBE_NOW = 'SET_SUBSCRIBE_NOW';
|
||||
|
||||
export default {
|
||||
COMPOSE_ADDED,
|
||||
COMPOSE_FAILED,
|
||||
COMPOSE_UPDATED,
|
||||
COMPOSES_UPDATED_COUNT,
|
||||
COMPOSE_UPDATED_STATUS,
|
||||
SET_RELEASE,
|
||||
SET_UPLOAD_DESTINATIONS,
|
||||
SET_UPLOAD_AWS,
|
||||
SET_UPLOAD_AZURE,
|
||||
SET_UPLOAD_GOOGLE,
|
||||
SET_SELECTED_PACKAGES,
|
||||
SET_SUBSCRIPTION,
|
||||
SET_SUBSCRIBE_NOW,
|
||||
COMPOSE_ADDED,
|
||||
COMPOSE_FAILED,
|
||||
COMPOSE_UPDATED,
|
||||
COMPOSES_UPDATED_COUNT,
|
||||
COMPOSE_UPDATED_STATUS,
|
||||
SET_RELEASE,
|
||||
SET_UPLOAD_DESTINATIONS,
|
||||
SET_UPLOAD_AWS,
|
||||
SET_UPLOAD_AZURE,
|
||||
SET_UPLOAD_GOOGLE,
|
||||
SET_SELECTED_PACKAGES,
|
||||
SET_SUBSCRIPTION,
|
||||
SET_SUBSCRIBE_NOW,
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,471 +10,498 @@ import { RHEL_8 } from '../../../constants.js';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../store/actions/actions', () => {
|
||||
return {
|
||||
composesGet: () => ({ type: 'foo' }),
|
||||
composeGetStatus: () => ({ type: 'bar' })
|
||||
};
|
||||
return {
|
||||
composesGet: () => ({ type: 'foo' }),
|
||||
composeGetStatus: () => ({ type: 'bar' }),
|
||||
};
|
||||
});
|
||||
|
||||
const store = {
|
||||
composes: {
|
||||
errors: null,
|
||||
allIds: [
|
||||
'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa',
|
||||
'edbae1c2-62bc-42c1-ae0c-3110ab718f58',
|
||||
'42ad0826-30b5-4f64-a24e-957df26fd564',
|
||||
'955944a2-e149-4058-8ac1-35b514cb5a16',
|
||||
'f7a60094-b376-4b58-a102-5c8c82dfd18b',
|
||||
'1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
|
||||
'61b0effa-c901-4ee5-86b9-2010b47f1b22',
|
||||
'ca03f120-9840-4959-871e-94a5cb49d1f2',
|
||||
'551de6f6-1533-4b46-a69f-7924051f9bc6',
|
||||
'77fa8b03-7efb-4120-9a20-da66d68c4494',
|
||||
],
|
||||
byId: {
|
||||
'1579d95b-8f1d-4982-8c53-8c2afa4ab04c': {
|
||||
id: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
|
||||
image_name: 'testImageName',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
ami: 'ami-0217b81d9be50e44b',
|
||||
region: 'us-east-1'
|
||||
},
|
||||
status: 'success',
|
||||
type: 'aws'
|
||||
}
|
||||
},
|
||||
composes: {
|
||||
errors: null,
|
||||
allIds: [
|
||||
'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa',
|
||||
'edbae1c2-62bc-42c1-ae0c-3110ab718f58',
|
||||
'42ad0826-30b5-4f64-a24e-957df26fd564',
|
||||
'955944a2-e149-4058-8ac1-35b514cb5a16',
|
||||
'f7a60094-b376-4b58-a102-5c8c82dfd18b',
|
||||
'1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
|
||||
'61b0effa-c901-4ee5-86b9-2010b47f1b22',
|
||||
'ca03f120-9840-4959-871e-94a5cb49d1f2',
|
||||
'551de6f6-1533-4b46-a69f-7924051f9bc6',
|
||||
'77fa8b03-7efb-4120-9a20-da66d68c4494',
|
||||
],
|
||||
byId: {
|
||||
'1579d95b-8f1d-4982-8c53-8c2afa4ab04c': {
|
||||
id: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
|
||||
image_name: 'testImageName',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
// kept "running" for backward compatibility
|
||||
'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa': {
|
||||
id: 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'running',
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
ami: 'ami-0217b81d9be50e44b',
|
||||
region: 'us-east-1',
|
||||
},
|
||||
'edbae1c2-62bc-42c1-ae0c-3110ab718f58': {
|
||||
id: 'edbae1c2-62bc-42c1-ae0c-3110ab718f58',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'pending',
|
||||
},
|
||||
status: 'success',
|
||||
type: 'aws',
|
||||
},
|
||||
},
|
||||
},
|
||||
// kept "running" for backward compatibility
|
||||
'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa': {
|
||||
id: 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
'42ad0826-30b5-4f64-a24e-957df26fd564': {
|
||||
id: '42ad0826-30b5-4f64-a24e-957df26fd564',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'building',
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'running',
|
||||
},
|
||||
},
|
||||
'edbae1c2-62bc-42c1-ae0c-3110ab718f58': {
|
||||
id: 'edbae1c2-62bc-42c1-ae0c-3110ab718f58',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
'955944a2-e149-4058-8ac1-35b514cb5a16': {
|
||||
id: '955944a2-e149-4058-8ac1-35b514cb5a16',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'uploading',
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'pending',
|
||||
},
|
||||
},
|
||||
'42ad0826-30b5-4f64-a24e-957df26fd564': {
|
||||
id: '42ad0826-30b5-4f64-a24e-957df26fd564',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
'f7a60094-b376-4b58-a102-5c8c82dfd18b': {
|
||||
id: 'f7a60094-b376-4b58-a102-5c8c82dfd18b',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'registering',
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'building',
|
||||
},
|
||||
},
|
||||
'955944a2-e149-4058-8ac1-35b514cb5a16': {
|
||||
id: '955944a2-e149-4058-8ac1-35b514cb5a16',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
'61b0effa-c901-4ee5-86b9-2010b47f1b22': {
|
||||
id: '61b0effa-c901-4ee5-86b9-2010b47f1b22',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'failure',
|
||||
error: {
|
||||
reason: 'A dependency error occured',
|
||||
details: {
|
||||
reason: 'Error in depsolve job'
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'uploading',
|
||||
},
|
||||
},
|
||||
'f7a60094-b376-4b58-a102-5c8c82dfd18b': {
|
||||
id: 'f7a60094-b376-4b58-a102-5c8c82dfd18b',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
'ca03f120-9840-4959-871e-94a5cb49d1f2': {
|
||||
id: 'ca03f120-9840-4959-871e-94a5cb49d1f2',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vhd',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: [
|
||||
'serviceAccount:test@email.com'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
image_name: 'composer-api-d446d8cb-7c16-4756-bf7d-706293785b05',
|
||||
project_id: 'red-hat-image-builder'
|
||||
},
|
||||
status: 'success',
|
||||
type: 'gcp'
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'registering',
|
||||
},
|
||||
},
|
||||
'61b0effa-c901-4ee5-86b9-2010b47f1b22': {
|
||||
id: '61b0effa-c901-4ee5-86b9-2010b47f1b22',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
'551de6f6-1533-4b46-a69f-7924051f9bc6': {
|
||||
id: '551de6f6-1533-4b46-a69f-7924051f9bc6',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vhd',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'building',
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'failure',
|
||||
error: {
|
||||
reason: 'A dependency error occured',
|
||||
details: {
|
||||
reason: 'Error in depsolve job',
|
||||
},
|
||||
'77fa8b03-7efb-4120-9a20-da66d68c4494': {
|
||||
id: '77fa8b03-7efb-4120-9a20-da66d68c4494',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vhd',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {
|
||||
tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2',
|
||||
subscription_id: 'test-subscription-id',
|
||||
resource_group: 'test-resource-group'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
image_name: 'composer-api-cc5920c3-5451-4282-aab3-725d3df7f1cb'
|
||||
},
|
||||
status: 'success',
|
||||
type: 'azure'
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
'ca03f120-9840-4959-871e-94a5cb49d1f2': {
|
||||
id: 'ca03f120-9840-4959-871e-94a5cb49d1f2',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vhd',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: ['serviceAccount:test@email.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'b7193673-8dcc-4a5f-ac30-e9f4940d8346': {
|
||||
created_at: '2022-01-11 13:33:33.767002 +0000 UTC',
|
||||
id: 'b7193673-8dcc-4a5f-ac30-e9f4940d8346',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vsphere',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
url: 'https://s3.amazonaws.com/b7193673-8dcc-4a5f-ac30-e9f4940d8346-disk.vmdk'
|
||||
},
|
||||
status: 'success',
|
||||
type: 'aws.s3'
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
image_name: 'composer-api-d446d8cb-7c16-4756-bf7d-706293785b05',
|
||||
project_id: 'red-hat-image-builder',
|
||||
},
|
||||
'4873fd0f-1851-4b9f-b4fe-4639fce90794': {
|
||||
created_at: '2022-01-11 13:33:33.767002 +0000 UTC',
|
||||
id: '4873fd0f-1851-4b9f-b4fe-4639fce90793',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'image-installer',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
url: 'https://s3.amazonaws.com/4873fd0f-1851-4b9f-b4fe-4639fce90794-installer.iso'
|
||||
},
|
||||
status: 'success',
|
||||
type: 'aws.s3'
|
||||
}
|
||||
}
|
||||
status: 'success',
|
||||
type: 'gcp',
|
||||
},
|
||||
},
|
||||
},
|
||||
'551de6f6-1533-4b46-a69f-7924051f9bc6': {
|
||||
id: '551de6f6-1533-4b46-a69f-7924051f9bc6',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vhd',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
'7b7d0d51-7106-42ab-98f2-f89872a9d599': {
|
||||
created_at: '2022-01-11 13:33:33.767002 +0000 UTC',
|
||||
id: '7b7d0d51-7106-42ab-98f2-f89872a9d599',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'guest-image',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3'
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'building',
|
||||
},
|
||||
},
|
||||
'77fa8b03-7efb-4120-9a20-da66d68c4494': {
|
||||
id: '77fa8b03-7efb-4120-9a20-da66d68c4494',
|
||||
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vhd',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {
|
||||
tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2',
|
||||
subscription_id: 'test-subscription-id',
|
||||
resource_group: 'test-resource-group',
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
url: 'https://s3.amazonaws.com/7b7d0d51-7106-42ab-98f2-f89872a9d599-disk.qcow2'
|
||||
},
|
||||
status: 'success',
|
||||
type: 'aws.s3'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
image_name: 'composer-api-cc5920c3-5451-4282-aab3-725d3df7f1cb',
|
||||
},
|
||||
status: 'success',
|
||||
type: 'azure',
|
||||
},
|
||||
},
|
||||
},
|
||||
'b7193673-8dcc-4a5f-ac30-e9f4940d8346': {
|
||||
created_at: '2022-01-11 13:33:33.767002 +0000 UTC',
|
||||
id: 'b7193673-8dcc-4a5f-ac30-e9f4940d8346',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vsphere',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
url: 'https://s3.amazonaws.com/b7193673-8dcc-4a5f-ac30-e9f4940d8346-disk.vmdk',
|
||||
},
|
||||
status: 'success',
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
},
|
||||
'4873fd0f-1851-4b9f-b4fe-4639fce90794': {
|
||||
created_at: '2022-01-11 13:33:33.767002 +0000 UTC',
|
||||
id: '4873fd0f-1851-4b9f-b4fe-4639fce90793',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'image-installer',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
url: 'https://s3.amazonaws.com/4873fd0f-1851-4b9f-b4fe-4639fce90794-installer.iso',
|
||||
},
|
||||
status: 'success',
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
},
|
||||
'7b7d0d51-7106-42ab-98f2-f89872a9d599': {
|
||||
created_at: '2022-01-11 13:33:33.767002 +0000 UTC',
|
||||
id: '7b7d0d51-7106-42ab-98f2-f89872a9d599',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'guest-image',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
url: 'https://s3.amazonaws.com/7b7d0d51-7106-42ab-98f2-f89872a9d599-disk.qcow2',
|
||||
},
|
||||
status: 'success',
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('Images Table', () => {
|
||||
test('render ImagesTable', () => {
|
||||
renderWithReduxRouter(<ImagesTable />, store);
|
||||
// make sure the empty-state message isn't present
|
||||
const emptyState = screen.queryByTestId('empty-state');
|
||||
expect(emptyState).not.toBeInTheDocument();
|
||||
test('render ImagesTable', () => {
|
||||
renderWithReduxRouter(<ImagesTable />, store);
|
||||
// make sure the empty-state message isn't present
|
||||
const emptyState = screen.queryByTestId('empty-state');
|
||||
expect(emptyState).not.toBeInTheDocument();
|
||||
|
||||
// check table
|
||||
const table = screen.getByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
// remove first row from list since it is just header labels
|
||||
const header = rows.shift();
|
||||
// test the header has correct labels
|
||||
expect(header.cells[1]).toHaveTextContent('Image name');
|
||||
expect(header.cells[2]).toHaveTextContent('Created');
|
||||
expect(header.cells[3]).toHaveTextContent('Release');
|
||||
expect(header.cells[4]).toHaveTextContent('Target');
|
||||
expect(header.cells[5]).toHaveTextContent('Status');
|
||||
expect(header.cells[6]).toHaveTextContent('Instance');
|
||||
// check table
|
||||
const table = screen.getByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
// remove first row from list since it is just header labels
|
||||
const header = rows.shift();
|
||||
// test the header has correct labels
|
||||
expect(header.cells[1]).toHaveTextContent('Image name');
|
||||
expect(header.cells[2]).toHaveTextContent('Created');
|
||||
expect(header.cells[3]).toHaveTextContent('Release');
|
||||
expect(header.cells[4]).toHaveTextContent('Target');
|
||||
expect(header.cells[5]).toHaveTextContent('Status');
|
||||
expect(header.cells[6]).toHaveTextContent('Instance');
|
||||
|
||||
// 10 rows for 10 images
|
||||
expect(rows).toHaveLength(10);
|
||||
for (const row of rows) {
|
||||
const col1 = row.cells[1].textContent;
|
||||
// 10 rows for 10 images
|
||||
expect(rows).toHaveLength(10);
|
||||
for (const row of rows) {
|
||||
const col1 = row.cells[1].textContent;
|
||||
|
||||
const composes = Object.values(store.composes.byId);
|
||||
// find compose with either the user defined image name or the uuid
|
||||
const compose = composes.find(compose => compose?.image_name === col1 || compose.id === col1);
|
||||
expect(compose).toBeTruthy();
|
||||
const composes = Object.values(store.composes.byId);
|
||||
// find compose with either the user defined image name or the uuid
|
||||
const compose = composes.find(
|
||||
(compose) => compose?.image_name === col1 || compose.id === col1
|
||||
);
|
||||
expect(compose).toBeTruthy();
|
||||
|
||||
// date should match the month day and year of the timestamp.
|
||||
expect(row.cells[2]).toHaveTextContent('Apr 27, 2021');
|
||||
// date should match the month day and year of the timestamp.
|
||||
expect(row.cells[2]).toHaveTextContent('Apr 27, 2021');
|
||||
|
||||
// render the expected <ImageBuildStatus /> and compare the text content
|
||||
let testElement = document.createElement('testElement');
|
||||
render(<Target
|
||||
imageType={ compose.request.image_requests[0].image_type }
|
||||
uploadType={ compose.request.image_requests[0].upload_request.type } />, { container: testElement });
|
||||
expect(row.cells[4]).toHaveTextContent(testElement.textContent);
|
||||
// render the expected <ImageBuildStatus /> and compare the text content
|
||||
let testElement = document.createElement('testElement');
|
||||
render(
|
||||
<Target
|
||||
imageType={compose.request.image_requests[0].image_type}
|
||||
uploadType={compose.request.image_requests[0].upload_request.type}
|
||||
/>,
|
||||
{ container: testElement }
|
||||
);
|
||||
expect(row.cells[4]).toHaveTextContent(testElement.textContent);
|
||||
|
||||
// render the expected <ImageBuildStatus /> and compare the text content
|
||||
render(<ImageBuildStatus status={ compose.image_status.status } />, { container: testElement });
|
||||
expect(row.cells[5]).toHaveTextContent(testElement.textContent);
|
||||
// render the expected <ImageBuildStatus /> and compare the text content
|
||||
render(<ImageBuildStatus status={compose.image_status.status} />, {
|
||||
container: testElement,
|
||||
});
|
||||
expect(row.cells[5]).toHaveTextContent(testElement.textContent);
|
||||
|
||||
// render the expected <ImageLink /> and compare the text content for a link
|
||||
render(
|
||||
<ImageLink imageStatus={ compose.image_status } uploadOptions={ compose.request.image_requests[0].upload_request.options } />,
|
||||
{ container: testElement }
|
||||
);
|
||||
expect(row.cells[6]).toHaveTextContent(testElement.textContent);
|
||||
}
|
||||
// render the expected <ImageLink /> and compare the text content for a link
|
||||
render(
|
||||
<ImageLink
|
||||
imageStatus={compose.image_status}
|
||||
uploadOptions={
|
||||
compose.request.image_requests[0].upload_request.options
|
||||
}
|
||||
/>,
|
||||
{ container: testElement }
|
||||
);
|
||||
expect(row.cells[6]).toHaveTextContent(testElement.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
test('check recreate action', () => {
|
||||
const { history } = renderWithReduxRouter(<ImagesTable />, store);
|
||||
|
||||
// get rows
|
||||
const table = screen.getByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
|
||||
// first row is header so look at index 1
|
||||
const imageId = rows[1].cells[1].textContent;
|
||||
|
||||
const actionsButton = within(rows[1]).getByRole('button', {
|
||||
name: 'Actions',
|
||||
});
|
||||
userEvent.click(actionsButton);
|
||||
const recreateButton = screen.getByRole('button', {
|
||||
name: 'Recreate image',
|
||||
});
|
||||
userEvent.click(recreateButton);
|
||||
|
||||
expect(history.location.pathname).toBe('/imagewizard');
|
||||
expect(history.location.state.composeRequest).toStrictEqual(
|
||||
store.composes.byId[imageId].request
|
||||
);
|
||||
expect(history.location.state.initialStep).toBe('review');
|
||||
});
|
||||
|
||||
test('check expandable row toggle', () => {
|
||||
renderWithReduxRouter(<ImagesTable />, store);
|
||||
|
||||
const table = screen.getByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
|
||||
const toggleButton = within(rows[6]).getByRole('button', {
|
||||
name: /details/i,
|
||||
});
|
||||
|
||||
test('check recreate action', () => {
|
||||
const { history } = renderWithReduxRouter(<ImagesTable />, store);
|
||||
expect(
|
||||
screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]
|
||||
).not.toBeVisible();
|
||||
userEvent.click(toggleButton);
|
||||
expect(
|
||||
screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]
|
||||
).toBeVisible();
|
||||
userEvent.click(toggleButton);
|
||||
expect(
|
||||
screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
// get rows
|
||||
const table = screen.getByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
test('check error details', () => {
|
||||
renderWithReduxRouter(<ImagesTable />, store);
|
||||
|
||||
// first row is header so look at index 1
|
||||
const imageId = rows[1].cells[1].textContent;
|
||||
const table = screen.getByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
|
||||
const actionsButton = within(rows[1]).getByRole('button', {
|
||||
name: 'Actions'
|
||||
});
|
||||
userEvent.click(actionsButton);
|
||||
const recreateButton = screen.getByRole('button', {
|
||||
name: 'Recreate image'
|
||||
});
|
||||
userEvent.click(recreateButton);
|
||||
|
||||
expect(history.location.pathname).toBe('/imagewizard');
|
||||
expect(history.location.state.composeRequest).toStrictEqual(store.composes.byId[imageId].request);
|
||||
expect(history.location.state.initialStep).toBe('review');
|
||||
const errorToggle = within(rows[7]).getByRole('button', {
|
||||
name: /details/i,
|
||||
});
|
||||
|
||||
test('check expandable row toggle', () => {
|
||||
renderWithReduxRouter(<ImagesTable />, store);
|
||||
expect(
|
||||
screen.getAllByText(/61b0effa-c901-4ee5-86b9-2010b47f1b22/i)[1]
|
||||
).not.toBeVisible();
|
||||
userEvent.click(errorToggle);
|
||||
|
||||
const table = screen.getByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
|
||||
const toggleButton = within(rows[6]).getByRole('button', { name: /details/i });
|
||||
|
||||
expect(screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]).not.toBeVisible();
|
||||
userEvent.click(toggleButton);
|
||||
expect(screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]).toBeVisible();
|
||||
userEvent.click(toggleButton);
|
||||
expect(screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('check error details', () => {
|
||||
renderWithReduxRouter(<ImagesTable />, store);
|
||||
|
||||
const table = screen.getByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
|
||||
const errorToggle = within(rows[7]).getByRole('button', { name: /details/i });
|
||||
|
||||
expect(screen.getAllByText(/61b0effa-c901-4ee5-86b9-2010b47f1b22/i)[1]).not.toBeVisible();
|
||||
userEvent.click(errorToggle);
|
||||
|
||||
expect(screen.getAllByText(/61b0effa-c901-4ee5-86b9-2010b47f1b22/i)[1]).toBeVisible();
|
||||
expect(screen.getAllByText(/Error in depsolve job/i)[0]).toBeVisible();
|
||||
});
|
||||
expect(
|
||||
screen.getAllByText(/61b0effa-c901-4ee5-86b9-2010b47f1b22/i)[1]
|
||||
).toBeVisible();
|
||||
expect(screen.getAllByText(/Error in depsolve job/i)[0]).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Images Table Toolbar', () => {
|
||||
test('render toolbar', () => {
|
||||
renderWithReduxRouter(<ImagesTable />, store);
|
||||
// check create image button
|
||||
screen.getByTestId('create-image-action');
|
||||
test('render toolbar', () => {
|
||||
renderWithReduxRouter(<ImagesTable />, store);
|
||||
// check create image button
|
||||
screen.getByTestId('create-image-action');
|
||||
|
||||
// check pagination renders
|
||||
screen.getByTestId('images-pagination');
|
||||
});
|
||||
// check pagination renders
|
||||
screen.getByTestId('images-pagination');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,28 +5,28 @@ import LandingPage from '../../../Components/LandingPage/LandingPage';
|
|||
import api from '../../../api.js';
|
||||
|
||||
jest.mock('../../../store/actions/actions', () => {
|
||||
return {
|
||||
composesGet: () => ({ type: 'foo' }),
|
||||
composeGetStatus: () => ({ type: 'bar' })
|
||||
};
|
||||
return {
|
||||
composesGet: () => ({ type: 'foo' }),
|
||||
composeGetStatus: () => ({ type: 'bar' }),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Landing Page', () => {
|
||||
test('renders page heading', async () => {
|
||||
renderWithReduxRouter(<LandingPage />);
|
||||
test('renders page heading', async () => {
|
||||
renderWithReduxRouter(<LandingPage />);
|
||||
|
||||
const composeImage = jest.spyOn(api, 'getVersion');
|
||||
composeImage.mockResolvedValue({ version: '1.0' });
|
||||
// check heading
|
||||
screen.getByRole('heading', { name: /Image Builder/i });
|
||||
});
|
||||
const composeImage = jest.spyOn(api, 'getVersion');
|
||||
composeImage.mockResolvedValue({ version: '1.0' });
|
||||
// check heading
|
||||
screen.getByRole('heading', { name: /Image Builder/i });
|
||||
});
|
||||
|
||||
test('renders EmptyState child component', async () => {
|
||||
renderWithReduxRouter(<LandingPage />);
|
||||
test('renders EmptyState child component', async () => {
|
||||
renderWithReduxRouter(<LandingPage />);
|
||||
|
||||
// check action loads
|
||||
screen.getByTestId('create-image-action');
|
||||
// check table loads
|
||||
screen.getByTestId('empty-state');
|
||||
});
|
||||
// check action loads
|
||||
screen.getByTestId('create-image-action');
|
||||
// check table loads
|
||||
screen.getByTestId('empty-state');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,32 +3,32 @@ import types from '../../store/types';
|
|||
import { RHEL_8 } from '../../constants.js';
|
||||
|
||||
const compose = {
|
||||
'77e4c693-0497-4b85-936d-b2a3ad69571b': {
|
||||
id: '77e4c693-0497-4b85-936d-b2a3ad69571b',
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
image_status: {
|
||||
status: 'uploading',
|
||||
'77e4c693-0497-4b85-936d-b2a3ad69571b': {
|
||||
id: '77e4c693-0497-4b85-936d-b2a3ad69571b',
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
image_status: {
|
||||
status: 'uploading',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('composeUpdated', () => {
|
||||
test('returns dict', () => {
|
||||
const result = actions.composeUpdated(compose);
|
||||
test('returns dict', () => {
|
||||
const result = actions.composeUpdated(compose);
|
||||
|
||||
// this function updates the type attribute and
|
||||
// returns everything else unchanged
|
||||
expect(result.type).toBe(types.COMPOSE_UPDATED);
|
||||
expect(result.payload.compose).toBe(compose);
|
||||
});
|
||||
// this function updates the type attribute and
|
||||
// returns everything else unchanged
|
||||
expect(result.type).toBe(types.COMPOSE_UPDATED);
|
||||
expect(result.payload.compose).toBe(compose);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,109 +3,105 @@ import types from '../../store/types';
|
|||
import { RHEL_8 } from '../../constants.js';
|
||||
|
||||
const compose = {
|
||||
id: '77e4c693-0497-4b85-936d-b2a3ad69571b',
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
image_status: {
|
||||
status: 'uploading',
|
||||
id: '77e4c693-0497-4b85-936d-b2a3ad69571b',
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'ami',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
image_status: {
|
||||
status: 'uploading',
|
||||
},
|
||||
};
|
||||
|
||||
describe('composes', () => {
|
||||
test('returns state for unknown actions', () => {
|
||||
const result = composes({}, {
|
||||
type: 'THIS-IS-UNKNOWN',
|
||||
});
|
||||
test('returns state for unknown actions', () => {
|
||||
const result = composes(
|
||||
{},
|
||||
{
|
||||
type: 'THIS-IS-UNKNOWN',
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
test('returns updated state for types.COMPOSE_ADDED', () => {
|
||||
const state = {
|
||||
allIds: [],
|
||||
byId: {},
|
||||
count: 1,
|
||||
errors: null,
|
||||
};
|
||||
const result = composes(state, {
|
||||
type: types.COMPOSE_ADDED,
|
||||
payload: { compose },
|
||||
});
|
||||
|
||||
test('returns updated state for types.COMPOSE_ADDED', () => {
|
||||
const state = {
|
||||
allIds: [],
|
||||
byId: {},
|
||||
count: 1,
|
||||
errors: null,
|
||||
};
|
||||
const result = composes(state, {
|
||||
type: types.COMPOSE_ADDED,
|
||||
payload: { compose }
|
||||
});
|
||||
expect(result.allIds).toEqual(['77e4c693-0497-4b85-936d-b2a3ad69571b']);
|
||||
expect(result.byId['77e4c693-0497-4b85-936d-b2a3ad69571b']).toEqual(
|
||||
compose
|
||||
);
|
||||
expect(result.count).toEqual(1);
|
||||
expect(result.error).toEqual(null);
|
||||
});
|
||||
|
||||
expect(result.allIds)
|
||||
.toEqual([ '77e4c693-0497-4b85-936d-b2a3ad69571b' ]);
|
||||
expect(result.byId['77e4c693-0497-4b85-936d-b2a3ad69571b'])
|
||||
.toEqual(compose);
|
||||
expect(result.count)
|
||||
.toEqual(1);
|
||||
expect(result.error)
|
||||
.toEqual(null);
|
||||
test('returns updated state for types.COMPOSE_UPDATED', () => {
|
||||
const state = {
|
||||
allIds: ['77e4c693-0497-4b85-936d-b2a3ad69571b'],
|
||||
byId: {
|
||||
'77e4c693-0497-4b85-936d-b2a3ad69571b': {},
|
||||
},
|
||||
count: 2,
|
||||
error: null,
|
||||
};
|
||||
const result = composes(state, {
|
||||
type: types.COMPOSE_UPDATED,
|
||||
payload: { compose },
|
||||
});
|
||||
|
||||
test('returns updated state for types.COMPOSE_UPDATED', () => {
|
||||
const state = {
|
||||
allIds: [ '77e4c693-0497-4b85-936d-b2a3ad69571b' ],
|
||||
byId: {
|
||||
'77e4c693-0497-4b85-936d-b2a3ad69571b': {},
|
||||
},
|
||||
count: 2,
|
||||
error: null,
|
||||
};
|
||||
const result = composes(state, {
|
||||
type: types.COMPOSE_UPDATED,
|
||||
payload: { compose }
|
||||
});
|
||||
expect(result.allIds).toEqual(['77e4c693-0497-4b85-936d-b2a3ad69571b']);
|
||||
expect(result.byId['77e4c693-0497-4b85-936d-b2a3ad69571b']).toEqual(
|
||||
compose
|
||||
);
|
||||
expect(result.count).toEqual(2);
|
||||
expect(result.error).toEqual(null);
|
||||
});
|
||||
|
||||
expect(result.allIds)
|
||||
.toEqual([ '77e4c693-0497-4b85-936d-b2a3ad69571b' ]);
|
||||
expect(result.byId['77e4c693-0497-4b85-936d-b2a3ad69571b'])
|
||||
.toEqual(compose);
|
||||
expect(result.count)
|
||||
.toEqual(2);
|
||||
expect(result.error)
|
||||
.toEqual(null);
|
||||
test('returns updated state for types.COMPOSE_FAILED', () => {
|
||||
const state = {
|
||||
allIds: [],
|
||||
byId: {},
|
||||
count: 0,
|
||||
error: null,
|
||||
};
|
||||
const result = composes(state, {
|
||||
type: types.COMPOSE_FAILED,
|
||||
payload: { error: 'test error' },
|
||||
});
|
||||
|
||||
test('returns updated state for types.COMPOSE_FAILED', () => {
|
||||
const state = {
|
||||
allIds: [],
|
||||
byId: {},
|
||||
count: 0,
|
||||
error: null,
|
||||
};
|
||||
const result = composes(state, {
|
||||
type: types.COMPOSE_FAILED,
|
||||
payload: { error: 'test error' }
|
||||
});
|
||||
expect(result.error).toEqual('test error');
|
||||
});
|
||||
|
||||
expect(result.error)
|
||||
.toEqual('test error');
|
||||
test('returns updated state for types.COMPOSES_UPDATED_COUNT', () => {
|
||||
const state = {
|
||||
allIds: [],
|
||||
byId: {},
|
||||
count: 0,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const result = composes(state, {
|
||||
type: types.COMPOSES_UPDATED_COUNT,
|
||||
payload: { count: 1 },
|
||||
});
|
||||
|
||||
test('returns updated state for types.COMPOSES_UPDATED_COUNT', () => {
|
||||
const state = {
|
||||
allIds: [],
|
||||
byId: {},
|
||||
count: 0,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const result = composes(state, {
|
||||
type: types.COMPOSES_UPDATED_COUNT,
|
||||
payload: { count: 1 }
|
||||
});
|
||||
|
||||
expect(result.count)
|
||||
.toEqual(1);
|
||||
|
||||
});
|
||||
expect(result.count).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,16 +6,18 @@ import { createMemoryHistory } from 'history';
|
|||
import { init, clearStore } from '../store';
|
||||
|
||||
export const renderWithReduxRouter = (component, store = {}, route = '/') => {
|
||||
const history = createMemoryHistory({ initialEntries: [ route ]});
|
||||
clearStore();
|
||||
let reduxStore = init(store);
|
||||
return {
|
||||
...render(
|
||||
<Provider store={ reduxStore.getStore() }>
|
||||
<Router location={ history.location } navigator={ history }>{component}</Router>
|
||||
</Provider>
|
||||
),
|
||||
history,
|
||||
reduxStore
|
||||
};
|
||||
const history = createMemoryHistory({ initialEntries: [route] });
|
||||
clearStore();
|
||||
let reduxStore = init(store);
|
||||
return {
|
||||
...render(
|
||||
<Provider store={reduxStore.getStore()}>
|
||||
<Router location={history.location} navigator={history}>
|
||||
{component}
|
||||
</Router>
|
||||
</Provider>
|
||||
),
|
||||
history,
|
||||
reduxStore,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue