Compare commits

..

3 commits
main ... gating

Author SHA1 Message Date
Sanne Raymaekers
8f1ba74759 playwright: wait until distro and arch have been initialized
On-prem the distro and architecture are set after the wizard has been
opened. This triggers a reload of the image types and makes the tests
very flaky.
2025-08-19 15:54:05 +02:00
Sanne Raymaekers
1c3ed83889 squash 2025-08-19 15:01:22 +02:00
Sanne Raymaekers
59f500f5c8
plans: add gating tests
This tmt[0] test runs the playwright tests as gating tests. Having the
gating tests upstream avoids duplication across fedora and centos
dist-git repositories, and running them upstream should keep them in
working order.

[0]: https://tmt.readthedocs.io/en/stable/index.html
2025-08-18 15:45:43 +02:00
57 changed files with 1876 additions and 2818 deletions

View file

@ -1,257 +0,0 @@
---
name: Debian Image Builder Frontend CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
env:
NODE_VERSION: "18"
DEBIAN_FRONTEND: noninteractive
jobs:
build-and-test:
name: Build and Test Frontend
runs-on: ubuntu-latest
container:
image: node:18-bullseye
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install build dependencies
run: |
apt-get update
apt-get install -y \
build-essential \
git \
ca-certificates \
python3
- name: Install Node.js dependencies
run: |
npm ci
npm run build || echo "Build script not found"
- name: Run tests
run: |
if [ -f package.json ] && npm run test; then
npm test
else
echo "No test script found, skipping tests"
fi
- name: Run linting
run: |
if [ -f package.json ] && npm run lint; then
npm run lint
else
echo "No lint script found, skipping linting"
fi
- name: Build production bundle
run: |
if [ -f package.json ] && npm run build; then
npm run build
else
echo "No build script found"
fi
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: |
dist/
build/
retention-days: 30
package:
name: Package Frontend
runs-on: ubuntu-latest
container:
image: node:18-bullseye
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install build dependencies
run: |
apt-get update
apt-get install -y \
build-essential \
devscripts \
debhelper \
git \
ca-certificates \
python3
- name: Install Node.js dependencies
run: npm ci
- name: Build production bundle
run: |
if [ -f package.json ] && npm run build; then
npm run build
else
echo "No build script found"
fi
- name: Create debian directory
run: |
mkdir -p debian
cat > debian/control << EOF
Source: debian-image-builder-frontend
Section: web
Priority: optional
Maintainer: Debian Forge Team <team@debian-forge.org>
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
Standards-Version: 4.6.2
Package: debian-image-builder-frontend
Architecture: all
Depends: \${misc:Depends}, nodejs, nginx
Description: Debian Image Builder Frontend
Web-based frontend for Debian Image Builder with Cockpit integration.
Provides a user interface for managing image builds, blueprints,
and system configurations through a modern React application.
EOF
cat > debian/rules << EOF
#!/usr/bin/make -f
%:
dh \$@
override_dh_auto_install:
dh_auto_install
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
# Copy built frontend files
if [ -d dist ]; then
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
elif [ -d build ]; then
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
fi
# Copy source files for development
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
# Create nginx configuration
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
server {
listen 80;
server_name localhost;
root /usr/share/debian-image-builder-frontend;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://localhost:8080/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
}
NGINX_EOF
# Create cockpit manifest
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
{
"version": 1,
"manifest": {
"name": "debian-image-builder",
"version": "1.0.0",
"title": "Debian Image Builder",
"description": "Build and manage Debian atomic images",
"url": "/usr/share/debian-image-builder-frontend",
"icon": "debian-logo",
"requires": {
"cockpit": ">= 200"
}
}
}
COCKPIT_EOF
EOF
cat > debian/changelog << EOF
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
* Initial release
* Debian Image Builder Frontend with Cockpit integration
* React-based web interface for image management
-- Debian Forge Team <team@debian-forge.org> $(date -R)
EOF
cat > debian/compat << EOF
13
EOF
chmod +x debian/rules
- name: Build Debian package
run: |
dpkg-buildpackage -us -uc -b
ls -la ../*.deb
- name: Upload Debian package
uses: actions/upload-artifact@v4
with:
name: debian-image-builder-frontend-deb
path: ../*.deb
retention-days: 30
cockpit-integration:
name: Test Cockpit Integration
runs-on: ubuntu-latest
container:
image: node:18-bullseye
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install Node.js dependencies
run: npm ci
- name: Test cockpit integration
run: |
echo "Testing Cockpit integration..."
if [ -d cockpit ]; then
echo "Cockpit directory found:"
ls -la cockpit/
else
echo "No cockpit directory found"
fi
if [ -f package.json ]; then
echo "Package.json scripts:"
npm run
fi

View file

@ -1,257 +0,0 @@
---
name: Debian Image Builder Frontend CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
env:
NODE_VERSION: "18"
DEBIAN_FRONTEND: noninteractive
jobs:
build-and-test:
name: Build and Test Frontend
runs-on: ubuntu-latest
container:
image: node:18-bullseye
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install build dependencies
run: |
apt-get update
apt-get install -y \
build-essential \
git \
ca-certificates \
python3
- name: Install Node.js dependencies
run: |
npm ci
npm run build || echo "Build script not found"
- name: Run tests
run: |
if [ -f package.json ] && npm run test; then
npm test
else
echo "No test script found, skipping tests"
fi
- name: Run linting
run: |
if [ -f package.json ] && npm run lint; then
npm run lint
else
echo "No lint script found, skipping linting"
fi
- name: Build production bundle
run: |
if [ -f package.json ] && npm run build; then
npm run build
else
echo "No build script found"
fi
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: |
dist/
build/
retention-days: 30
package:
name: Package Frontend
runs-on: ubuntu-latest
container:
image: node:18-bullseye
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install build dependencies
run: |
apt-get update
apt-get install -y \
build-essential \
devscripts \
debhelper \
git \
ca-certificates \
python3
- name: Install Node.js dependencies
run: npm ci
- name: Build production bundle
run: |
if [ -f package.json ] && npm run build; then
npm run build
else
echo "No build script found"
fi
- name: Create debian directory
run: |
mkdir -p debian
cat > debian/control << EOF
Source: debian-image-builder-frontend
Section: web
Priority: optional
Maintainer: Debian Forge Team <team@debian-forge.org>
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
Standards-Version: 4.6.2
Package: debian-image-builder-frontend
Architecture: all
Depends: \${misc:Depends}, nodejs, nginx
Description: Debian Image Builder Frontend
Web-based frontend for Debian Image Builder with Cockpit integration.
Provides a user interface for managing image builds, blueprints,
and system configurations through a modern React application.
EOF
cat > debian/rules << EOF
#!/usr/bin/make -f
%:
dh \$@
override_dh_auto_install:
dh_auto_install
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
# Copy built frontend files
if [ -d dist ]; then
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
elif [ -d build ]; then
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
fi
# Copy source files for development
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
# Create nginx configuration
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
server {
listen 80;
server_name localhost;
root /usr/share/debian-image-builder-frontend;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://localhost:8080/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
}
NGINX_EOF
# Create cockpit manifest
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
{
"version": 1,
"manifest": {
"name": "debian-image-builder",
"version": "1.0.0",
"title": "Debian Image Builder",
"description": "Build and manage Debian atomic images",
"url": "/usr/share/debian-image-builder-frontend",
"icon": "debian-logo",
"requires": {
"cockpit": ">= 200"
}
}
}
COCKPIT_EOF
EOF
cat > debian/changelog << EOF
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
* Initial release
* Debian Image Builder Frontend with Cockpit integration
* React-based web interface for image management
-- Debian Forge Team <team@debian-forge.org> $(date -R)
EOF
cat > debian/compat << EOF
13
EOF
chmod +x debian/rules
- name: Build Debian package
run: |
dpkg-buildpackage -us -uc -b
ls -la ../*.deb
- name: Upload Debian package
uses: actions/upload-artifact@v4
with:
name: debian-image-builder-frontend-deb
path: ../*.deb
retention-days: 30
cockpit-integration:
name: Test Cockpit Integration
runs-on: ubuntu-latest
container:
image: node:18-bullseye
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install Node.js dependencies
run: npm ci
- name: Test cockpit integration
run: |
echo "Testing Cockpit integration..."
if [ -d cockpit ]; then
echo "Cockpit directory found:"
ls -la cockpit/
else
echo "No cockpit directory found"
fi
if [ -f package.json ]; then
echo "Package.json scripts:"
npm run
fi

View file

@ -32,7 +32,8 @@ test:
- RUNNER:
- aws/fedora-41-x86_64
- aws/fedora-42-x86_64
- aws/rhel-10.1-nightly-x86_64
- aws/rhel-9.6-nightly-x86_64
- aws/rhel-10.0-nightly-x86_64
INTERNAL_NETWORK: ["true"]
finish:

View file

@ -194,7 +194,7 @@ spec:
- name: name
value: prefetch-dependencies
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:76bc23ca84c6b31251fee267f417ed262d9ea49655e942d1978149e137295bb0
- name: kind
value: task
resolver: bundles
@ -238,7 +238,7 @@ spec:
- name: name
value: buildah
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:09ca1ce263bb686b08187a3b836f6a5bd54875e8596105e4d7d816d57caf55a0
- name: kind
value: task
resolver: bundles
@ -413,7 +413,7 @@ spec:
- name: name
value: ecosystem-cert-preflight-checks
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:4bafcaab0f0c998a89a1cc33bdbbf74f39eea52e6c0e43013c356a322f94940f
- name: kind
value: task
resolver: bundles
@ -503,7 +503,7 @@ spec:
- name: name
value: push-dockerfile
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:5446102f233991bdc73451201adf361766d45c638f3d89f19121ae1c2ba8bf17
- name: kind
value: task
resolver: bundles

View file

@ -191,7 +191,7 @@ spec:
- name: name
value: prefetch-dependencies
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:76bc23ca84c6b31251fee267f417ed262d9ea49655e942d1978149e137295bb0
- name: kind
value: task
resolver: bundles
@ -235,7 +235,7 @@ spec:
- name: name
value: buildah
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:09ca1ce263bb686b08187a3b836f6a5bd54875e8596105e4d7d816d57caf55a0
- name: kind
value: task
resolver: bundles
@ -410,7 +410,7 @@ spec:
- name: name
value: ecosystem-cert-preflight-checks
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:4bafcaab0f0c998a89a1cc33bdbbf74f39eea52e6c0e43013c356a322f94940f
- name: kind
value: task
resolver: bundles
@ -500,7 +500,7 @@ spec:
- name: name
value: push-dockerfile
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:5446102f233991bdc73451201adf361766d45c638f3d89f19121ae1c2ba8bf17
- name: kind
value: task
resolver: bundles

View file

@ -1,5 +1,5 @@
Name: cockpit-image-builder
Version: 76
Version: 74
Release: 1%{?dist}
Summary: Image builder plugin for Cockpit

1998
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,17 +8,16 @@
},
"dependencies": {
"@ltd/j-toml": "1.38.0",
"@patternfly/patternfly": "6.3.1",
"@patternfly/patternfly": "6.3.0",
"@patternfly/react-code-editor": "6.3.1",
"@patternfly/react-core": "6.3.1",
"@patternfly/react-core": "6.3.0",
"@patternfly/react-table": "6.3.1",
"@redhat-cloud-services/frontend-components": "7.0.3",
"@redhat-cloud-services/frontend-components-notifications": "6.1.5",
"@redhat-cloud-services/frontend-components-notifications": "6.1.3",
"@redhat-cloud-services/frontend-components-utilities": "7.0.3",
"@redhat-cloud-services/types": "3.0.1",
"@reduxjs/toolkit": "2.8.2",
"@scalprum/react-core": "0.9.5",
"@sentry/webpack-plugin": "4.1.1",
"@sentry/webpack-plugin": "4.1.0",
"@unleash/proxy-client-react": "5.0.1",
"classnames": "2.5.1",
"jwt-decode": "4.0.0",
@ -47,13 +46,13 @@
"@testing-library/jest-dom": "6.6.4",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/node": "24.3.0",
"@types/node": "24.1.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@types/react-redux": "7.1.34",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@typescript-eslint/eslint-plugin": "8.39.0",
"@typescript-eslint/parser": "8.39.0",
"@vitejs/plugin-react": "4.7.0",
"@vitest/coverage-v8": "3.2.4",
"babel-loader": "10.0.0",
@ -62,13 +61,13 @@
"chartjs-plugin-annotation": "3.1.0",
"copy-webpack-plugin": "13.0.0",
"css-loader": "7.1.2",
"eslint": "9.33.0",
"eslint": "9.32.0",
"eslint-plugin-disable-autofix": "5.0.1",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jest-dom": "5.5.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-playwright": "2.2.2",
"eslint-plugin-prettier": "5.5.4",
"eslint-plugin-prettier": "5.5.3",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-redux": "4.2.2",
@ -81,7 +80,7 @@
"madge": "8.0.0",
"mini-css-extract-plugin": "2.9.2",
"moment": "2.30.1",
"msw": "2.10.5",
"msw": "2.10.4",
"npm-run-all": "4.1.5",
"path-browserify": "1.0.1",
"postcss-scss": "4.0.9",
@ -89,12 +88,12 @@
"redux-mock-store": "1.5.5",
"sass": "1.90.0",
"sass-loader": "16.0.5",
"stylelint": "16.23.1",
"stylelint-config-recommended-scss": "16.0.0",
"stylelint": "16.23.0",
"stylelint-config-recommended-scss": "15.0.1",
"ts-node": "10.9.2",
"ts-patch": "3.3.0",
"typescript": "5.8.3",
"typescript-eslint": "8.40.0",
"typescript-eslint": "8.38.0",
"uuid": "11.1.0",
"vitest": "3.2.4",
"vitest-canvas-mock": "0.3.3",

View file

@ -22,8 +22,10 @@ jobs:
tmt_plan: /plans/all/main
targets:
- centos-stream-10
- centos-stream-10-aarch64
- fedora-41
- fedora-42
- fedora-latest-stable-aarch64
- job: copr_build
trigger: pull_request

View file

@ -1,214 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { test } from '../fixtures/customizations';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import {
ibFrame,
navigateToLandingPage,
navigateToOptionalSteps,
} from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
const validCallbackUrl =
'https://controller.url/api/controller/v2/job_templates/9/callback/';
const validHttpCallbackUrl =
'http://controller.url/api/controller/v2/job_templates/9/callback/';
const validHostConfigKey = 'hostconfigkey';
const validCertificate = `-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAOEzx5ezZ9EIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAklOMQswCQYDVQQIDAJLUjEMMAoGA1UEBwwDS1JHMRAwDgYDVQQKDAdUZXN0
IENBMB4XDTI1MDUxNTEyMDAwMFoXDTI2MDUxNTEyMDAwMFowRTELMAkGA1UEBhMC
SU4xCzAJBgNVBAgMAktSMQwwCgYDVQQHDANSR0sxEDAOBgNVBAoMB1Rlc3QgQ0Ew
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+R4gfN5pyJQo5qBTTtN+7
eE9CSXZJ8SVVaE3U54IgqQoqsSoBY5QtExy7v5C6l6mW4E6dzK/JecmvTTO/BvlG
A5k2hxB6bOQxtxYwfgElH+RFWN9P4xxhtEiQgHoG1rDfnXuDJk1U3YEkCQELUebz
fF3EIDU1yR0Sz2bA+Sl2VXe8og1MEZfytq8VZUVltxtn2PfW7zI5gOllBR2sKeUc
K6h8HXN7qMgfEvsLIXxTw7fU/zA3ibcxfRCl3m6QhF8hwRh6F9Wtz2s8hCzGegV5
z0M39nY7X8C3GZQ4Ly8v8DdY+FbEix7K3SSBRbWtdPfAHRFlX9Er2Wf8DAr7O2hH
AgMBAAGjUDBOMB0GA1UdDgQWBBTXXz2eIDgK+BhzDUAGzptn0OMcpDAfBgNVHSME
GDAWgBTXXz2eIDgK+BhzDUAGzptn0OMcpDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQAoUgY4jsuBMB3el9cc7JS2rcOhhJzn47Hj2UANfJq52g5lbjo7
XDc7Wb3VDcV+1LzjdzayT1qO1WzHb6FDPW9L9f6h4s8lj6MvJ+xhOWgD11srdIt3
vbQaQW4zDfeVRcKXzqbcUX8BLXAdzJPqVwZ+Z4EDjYrJ7lF9k+IqfZm0MsYX7el9
kvdRHbLuF4Q0sZ05CXMFkhM0Ulhu4MZ+1FcsQa7nWfZzTmbjHOuWJPB4z5WwrB7z
U8YYvWJ3qxToWGbATqJxkRKGGqLrNrmwcfzgPqkpuCRYi0Kky6gJ1RvL+DRopY9x
uD+ckf3oH2wYAB6RpPRMkfVxe7lGMvq/yEZ6
-----END CERTIFICATE-----`;
const invalidCertificate = `-----BEGIN CERTIFICATE-----
ThisIs*Not+Valid/Base64==
-----END CERTIFICATE-----`;
test('Create a blueprint with AAP registration customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
// Skip entirely in Cockpit/on-premise where AAP customization is unavailable
test.skip(!isHosted(), 'AAP customization is not available in the plugin');
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Navigate to optional steps in Wizard', async () => {
await navigateToOptionalSteps(frame);
await registerLater(frame);
});
await test.step('Select and fill the AAP step with valid configuration', async () => {
await frame
.getByRole('button', { name: 'Ansible Automation Platform' })
.click();
await frame
.getByRole('textbox', { name: 'ansible callback url' })
.fill(validCallbackUrl);
await frame
.getByRole('textbox', { name: 'host config key' })
.fill(validHostConfigKey);
await frame
.getByRole('textbox', { name: 'File upload' })
.fill(validCertificate);
await expect(frame.getByRole('button', { name: 'Next' })).toBeEnabled();
});
await test.step('Test TLS confirmation checkbox for HTTPS URLs', async () => {
// TLS confirmation checkbox should appear for HTTPS URLs
await expect(
frame.getByRole('checkbox', {
name: 'Insecure',
}),
).toBeVisible();
// Check TLS confirmation and verify CA input is hidden
await frame
.getByRole('checkbox', {
name: 'Insecure',
})
.check();
await expect(
frame.getByRole('textbox', { name: 'File upload' }),
).toBeHidden();
await frame
.getByRole('checkbox', {
name: 'Insecure',
})
.uncheck();
await expect(
frame.getByRole('textbox', { name: 'File upload' }),
).toBeVisible();
});
await test.step('Test certificate validation', async () => {
await frame.getByRole('textbox', { name: 'File upload' }).clear();
await frame
.getByRole('textbox', { name: 'File upload' })
.fill(invalidCertificate);
await expect(frame.getByText(/Certificate.*is not valid/)).toBeVisible();
await frame.getByRole('textbox', { name: 'File upload' }).clear();
await frame
.getByRole('textbox', { name: 'File upload' })
.fill(validCertificate);
await expect(frame.getByText('Certificate was uploaded')).toBeVisible();
});
await test.step('Test HTTP URL behavior', async () => {
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
await frame
.getByRole('textbox', { name: 'ansible callback url' })
.fill(validHttpCallbackUrl);
// TLS confirmation checkbox should NOT appear for HTTP URLs
await expect(
frame.getByRole('checkbox', {
name: 'Insecure',
}),
).toBeHidden();
await expect(
frame.getByRole('textbox', { name: 'File upload' }),
).toBeVisible();
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
await frame
.getByRole('textbox', { name: 'ansible callback url' })
.fill(validCallbackUrl);
});
await test.step('Complete AAP configuration and proceed to review', async () => {
await frame.getByRole('button', { name: 'Review and finish' }).click();
});
await test.step('Fill the BP details', async () => {
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP and verify AAP configuration persists', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByLabel('Revisit Ansible Automation Platform step').click();
await expect(
frame.getByRole('textbox', { name: 'ansible callback url' }),
).toHaveValue(validCallbackUrl);
await expect(
frame.getByRole('textbox', { name: 'host config key' }),
).toHaveValue(validHostConfigKey);
await expect(
frame.getByRole('textbox', { name: 'File upload' }),
).toHaveValue(validCertificate);
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async (step) => {
step.skip(!isHosted(), 'Exporting is not available in the plugin');
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await fillInImageOutputGuest(page);
await page
.getByRole('button', { name: 'Ansible Automation Platform' })
.click();
await expect(
page.getByRole('textbox', { name: 'ansible callback url' }),
).toHaveValue(validCallbackUrl);
await expect(
page.getByRole('textbox', { name: 'host config key' }),
).toBeEmpty();
await expect(
page.getByRole('textbox', { name: 'File upload' }),
).toHaveValue(validCertificate);
await page.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -67,10 +67,8 @@ export const getHostDistroName = (): string => {
const lineData = l.split('=');
(osRel as any)[lineData[0]] = lineData[1].replace(/"/g, '');
}
// strip minor version from rhel
const distro = ON_PREM_RELEASES.get(
`${(osRel as any)['ID']}-${(osRel as any)['VERSION_ID'].split('.')[0]}`,
`${(osRel as any)['ID']}-${(osRel as any)['VERSION_ID']}`,
);
if (distro === undefined) {

View file

@ -72,11 +72,6 @@ test.describe.serial('test', () => {
frame.getByRole('heading', { name: 'Systemd services' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
if (isHosted()) {
frame.getByRole('heading', { name: 'Ansible Automation Platform' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
}
if (isHosted()) {
frame.getByRole('heading', { name: 'First boot configuration' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
@ -92,12 +87,7 @@ test.describe.serial('test', () => {
await frame.getByRole('button', { name: 'Create blueprint' }).click();
await expect(
frame.locator('.pf-v6-c-card__title-text').getByText(
// if the name is too long, the blueprint card will have a truncated name.
blueprintName.length > 24
? blueprintName.slice(0, 24) + '...'
: blueprintName,
),
frame.locator('.pf-v6-c-card__title-text').getByText(blueprintName),
).toBeVisible();
});

View file

@ -1 +1 @@
cf0a810fd3b75fa27139746c4dfe72222e13dcba
7b4735d287dd0950e0a6f47dde65b62b0f239da1

View file

@ -50,21 +50,11 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
onChange: () => dispatch(setBlueprintId(blueprint.id)),
}}
>
<CardTitle aria-label={blueprint.name}>
<CardTitle>
{isLoading && blueprint.id === selectedBlueprintId && (
<Spinner size='md' />
)}
{
// NOTE: This might be an issue with the pf6 truncate component.
// Since we're not really using the popover, we can just
// use vanilla js to truncate the string rather than use the
// Truncate component. We can match the behaviour of the component
// by also splitting on 24 characters.
// https://github.com/patternfly/patternfly-react/issues/11964
blueprint.name && blueprint.name.length > 24
? blueprint.name.slice(0, 24) + '...'
: blueprint.name
}
{blueprint.name}
</CardTitle>
</CardHeader>
<CardBody>{blueprint.description}</CardBody>

View file

@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import {
Bullseye,
@ -17,6 +17,7 @@ import {
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import debounce from 'lodash/debounce';
import { Link } from 'react-router-dom';
@ -28,7 +29,6 @@ import {
PAGINATION_LIMIT,
PAGINATION_OFFSET,
} from '../../constants';
import { useGetUser } from '../../Hooks';
import { useGetBlueprintsQuery } from '../../store/backendApi';
import {
selectBlueprintSearchInput,
@ -60,8 +60,8 @@ type emptyBlueprintStateProps = {
};
const BlueprintsSidebar = () => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
const { userData } = useGetUser(auth);
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
@ -73,6 +73,16 @@ const BlueprintsSidebar = () => {
offset: blueprintsOffset,
};
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (blueprintSearchInput) {
searchParams.search = blueprintSearchInput;
}

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Button,
@ -16,13 +16,11 @@ import {
} from '@patternfly/react-core';
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { skipToken } from '@reduxjs/toolkit/query';
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
import {
useComposeBPWithNotification as useComposeBlueprintMutation,
useGetUser,
} from '../../Hooks';
import { useComposeBPWithNotification as useComposeBlueprintMutation } from '../../Hooks';
import { useGetBlueprintQuery } from '../../store/backendApi';
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
import { useAppSelector } from '../../store/hooks';
@ -39,7 +37,18 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
useComposeBlueprintMutation();
const { analytics, auth } = useChrome();
const { userData } = useGetUser(auth);
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onBuildHandler = async () => {
if (selectedBlueprintId) {

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
Button,
@ -9,16 +9,14 @@ import {
ModalVariant,
} from '@patternfly/react-core';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import {
AMPLITUDE_MODULE_NAME,
PAGINATION_LIMIT,
PAGINATION_OFFSET,
} from '../../constants';
import {
useDeleteBPWithNotification as useDeleteBlueprintMutation,
useGetUser,
} from '../../Hooks';
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
import {
selectBlueprintSearchInput,
@ -44,7 +42,17 @@ export const DeleteBlueprintModal: React.FunctionComponent<
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
const dispatch = useAppDispatch();
const { analytics, auth } = useChrome();
const { userData } = useGetUser(auth);
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const searchParams: GetBlueprintsApiArg = {
limit: blueprintsLimit,

View file

@ -15,7 +15,6 @@ import { WizardStepType } from '@patternfly/react-core/dist/esm/components/Wizar
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { useNavigate, useSearchParams } from 'react-router-dom';
import AAPStep from './steps/AAP';
import DetailsStep from './steps/Details';
import FileSystemStep from './steps/FileSystem';
import { FileSystemContext } from './steps/FileSystem/components/FileSystemTable';
@ -41,7 +40,6 @@ import UsersStep from './steps/Users';
import { getHostArch, getHostDistro } from './utilities/getHostInfo';
import { useHasSpecificTargetOnly } from './utilities/hasSpecificTargetOnly';
import {
useAAPValidation,
useDetailsValidation,
useFilesystemValidation,
useFirewallValidation,
@ -199,7 +197,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
// Feature flags
const complianceEnabled = useFlag('image-builder.compliance.enabled');
const isAAPRegistrationEnabled = useFlag('image-builder.aap.enabled');
// IMPORTANT: Ensure the wizard starts with a fresh initial state
useEffect(() => {
@ -286,8 +283,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
const firewallValidation = useFirewallValidation();
// Services
const servicesValidation = useServicesValidation();
// AAP
const aapValidation = useAAPValidation();
// Firstboot
const firstBootValidation = useFirstBootValidation();
// Details
@ -298,10 +293,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
const hasWslTargetOnly = useHasSpecificTargetOnly('wsl');
let startIndex = 1; // default index
const JUMP_TO_REVIEW_STEP = 23;
if (isEdit) {
startIndex = JUMP_TO_REVIEW_STEP;
startIndex = 22;
}
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
@ -662,22 +655,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
>
<ServicesStep />
</WizardStep>,
<WizardStep
name='Ansible Automation Platform'
id='wizard-aap'
isHidden={!isAAPRegistrationEnabled}
key='wizard-aap'
navItem={CustomStatusNavItem}
status={aapValidation.disabledNext ? 'error' : 'default'}
footer={
<CustomWizardFooter
disableNext={aapValidation.disabledNext}
optional={true}
/>
}
>
<AAPStep />
</WizardStep>,
<WizardStep
name='First boot script configuration'
id='wizard-first-boot'

View file

@ -23,7 +23,7 @@ type ValidatedTextInputPropTypes = TextInputProps & {
type ValidationInputProp = TextInputProps &
TextAreaProps & {
value: string;
placeholder?: string;
placeholder: string;
stepValidation: StepValidation;
dataTestId?: string;
fieldName: string;
@ -91,7 +91,7 @@ export const ValidatedInputAndTextArea = ({
onChange={onChange}
validated={validated}
onBlur={handleBlur}
placeholder={placeholder || ''}
placeholder={placeholder}
aria-label={ariaLabel}
data-testid={dataTestId}
/>
@ -138,7 +138,6 @@ export const ValidatedInput = ({
value,
placeholder,
onChange,
...props
}: ValidatedTextInputPropTypes) => {
const [isPristine, setIsPristine] = useState(!value ? true : false);
@ -165,7 +164,6 @@ export const ValidatedInput = ({
aria-label={ariaLabel || ''}
onBlur={handleBlur}
placeholder={placeholder || ''}
{...props}
/>
{!isPristine && !validator(value) && (
<HelperText>

View file

@ -1,178 +0,0 @@
import React from 'react';
import {
Checkbox,
DropEvent,
FileUpload,
FormGroup,
FormHelperText,
HelperText,
HelperTextItem,
} from '@patternfly/react-core';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import {
changeAapCallbackUrl,
changeAapHostConfigKey,
changeAapTlsCertificateAuthority,
changeAapTlsConfirmation,
selectAapCallbackUrl,
selectAapHostConfigKey,
selectAapTlsCertificateAuthority,
selectAapTlsConfirmation,
} from '../../../../../store/wizardSlice';
import { useAAPValidation } from '../../../utilities/useValidation';
import { ValidatedInputAndTextArea } from '../../../ValidatedInput';
import { validateMultipleCertificates } from '../../../validators';
const AAPRegistration = () => {
const dispatch = useAppDispatch();
const callbackUrl = useAppSelector(selectAapCallbackUrl);
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
const tlsCertificateAuthority = useAppSelector(
selectAapTlsCertificateAuthority,
);
const tlsConfirmation = useAppSelector(selectAapTlsConfirmation);
const [isRejected, setIsRejected] = React.useState(false);
const stepValidation = useAAPValidation();
const isHttpsUrl = callbackUrl?.toLowerCase().startsWith('https://') || false;
const shouldShowCaInput = !isHttpsUrl || (isHttpsUrl && !tlsConfirmation);
const validated = stepValidation.errors['certificate']
? 'error'
: stepValidation.errors['certificate'] === undefined &&
tlsCertificateAuthority &&
validateMultipleCertificates(tlsCertificateAuthority).validCertificates
.length > 0
? 'success'
: 'default';
const handleCallbackUrlChange = (value: string) => {
dispatch(changeAapCallbackUrl(value));
};
const handleHostConfigKeyChange = (value: string) => {
dispatch(changeAapHostConfigKey(value));
};
const handleClear = () => {
dispatch(changeAapTlsCertificateAuthority(''));
};
const handleTextChange = (
_event: React.ChangeEvent<HTMLTextAreaElement>,
value: string,
) => {
dispatch(changeAapTlsCertificateAuthority(value));
setIsRejected(false);
};
const handleDataChange = (_: DropEvent, value: string) => {
dispatch(changeAapTlsCertificateAuthority(value));
setIsRejected(false);
};
const handleFileRejected = () => {
dispatch(changeAapTlsCertificateAuthority(''));
setIsRejected(true);
};
const handleTlsConfirmationChange = (checked: boolean) => {
dispatch(changeAapTlsConfirmation(checked));
};
return (
<>
<FormGroup label='Ansible Callback URL' isRequired>
<ValidatedInputAndTextArea
value={callbackUrl || ''}
onChange={(_event, value) => handleCallbackUrlChange(value.trim())}
ariaLabel='ansible callback url'
isRequired
stepValidation={stepValidation}
fieldName='callbackUrl'
/>
</FormGroup>
<FormGroup label='Host Config Key' isRequired>
<ValidatedInputAndTextArea
value={hostConfigKey || ''}
onChange={(_event, value) => handleHostConfigKeyChange(value.trim())}
ariaLabel='host config key'
isRequired
stepValidation={stepValidation}
fieldName='hostConfigKey'
/>
</FormGroup>
{shouldShowCaInput && (
<FormGroup label='Certificate authority (CA) for Ansible Controller'>
<FileUpload
id='aap-certificate-upload'
type='text'
value={tlsCertificateAuthority || ''}
filename={tlsCertificateAuthority ? 'CA detected' : ''}
onDataChange={handleDataChange}
onTextChange={handleTextChange}
onClearClick={handleClear}
dropzoneProps={{
accept: {
'application/x-pem-file': ['.pem'],
'application/x-x509-ca-cert': ['.cer', '.crt'],
'application/pkix-cert': ['.der'],
},
maxSize: 512000,
onDropRejected: handleFileRejected,
}}
validated={isRejected ? 'error' : validated}
browseButtonText='Upload'
allowEditingUploadedText={true}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
variant={
isRejected || validated === 'error'
? 'error'
: validated === 'success'
? 'success'
: 'default'
}
>
{isRejected
? 'Must be a .PEM/.CER/.CRT file'
: validated === 'error'
? stepValidation.errors['certificate']
: validated === 'success'
? 'Certificate was uploaded'
: 'Drag and drop a valid certificate file or upload one'}
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
)}
{isHttpsUrl && (
<FormGroup>
<Checkbox
id='tls-confirmation-checkbox'
label='Insecure'
isChecked={tlsConfirmation || false}
onChange={(_event, checked) => handleTlsConfirmationChange(checked)}
/>
{stepValidation.errors['tlsConfirmation'] && (
<FormHelperText>
<HelperText>
<HelperTextItem variant='error'>
{stepValidation.errors['tlsConfirmation']}
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
</FormGroup>
)}
</>
);
};
export default AAPRegistration;

View file

@ -1,18 +0,0 @@
import React from 'react';
import { Form, Title } from '@patternfly/react-core';
import AAPRegistration from './components/AAPRegistration';
const AAPStep = () => {
return (
<Form>
<Title headingLevel='h1' size='xl'>
Ansible Automation Platform
</Title>
<AAPRegistration />
</Form>
);
};
export default AAPStep;

View file

@ -8,10 +8,7 @@ import {
Spinner,
} from '@patternfly/react-core';
import {
useGetComplianceCustomizationsQuery,
useGetOscapCustomizationsQuery,
} from '../../../../../store/backendApi';
import { useGetOscapCustomizationsQuery } from '../../../../../store/backendApi';
import { PolicyRead, usePolicyQuery } from '../../../../../store/complianceApi';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import { OpenScapProfile } from '../../../../../store/imageBuilderApi';
@ -19,7 +16,6 @@ import {
changeCompliance,
selectCompliancePolicyID,
selectComplianceProfileID,
selectComplianceType,
selectDistribution,
selectFips,
} from '../../../../../store/wizardSlice';
@ -35,29 +31,12 @@ export const OscapProfileInformation = ({
const release = useAppSelector(selectDistribution);
const compliancePolicyID = useAppSelector(selectCompliancePolicyID);
const complianceProfileID = useAppSelector(selectComplianceProfileID);
const complianceType = useAppSelector(selectComplianceType);
const fips = useAppSelector(selectFips);
const {
data: oscapPolicyInfo,
isFetching: isFetchingOscapPolicyInfo,
isSuccess: isSuccessOscapPolicyInfo,
error: policyError,
} = useGetComplianceCustomizationsQuery(
{
distribution: release,
policy: compliancePolicyID!,
},
{
skip: !compliancePolicyID || !!process.env.IS_ON_PREMISE,
},
);
const {
data: oscapProfileInfo,
isFetching: isFetchingOscapProfileInfo,
isSuccess: isSuccessOscapProfileInfo,
error: profileError,
} = useGetOscapCustomizationsQuery(
{
distribution: release,
@ -69,20 +48,6 @@ export const OscapProfileInformation = ({
},
);
const customizationData =
compliancePolicyID && oscapPolicyInfo ? oscapPolicyInfo : oscapProfileInfo;
const profileMetadata = oscapProfileInfo;
const isPolicyDataLoading = compliancePolicyID
? isFetchingOscapPolicyInfo
: false;
const isFetchingOscapData = isPolicyDataLoading || isFetchingOscapProfileInfo;
const isPolicyDataSuccess = compliancePolicyID
? isSuccessOscapPolicyInfo
: true;
const isSuccessOscapData = isPolicyDataSuccess && isSuccessOscapProfileInfo;
const hasCriticalError = profileError || (compliancePolicyID && policyError);
const shouldShowData = isSuccessOscapData && !hasCriticalError;
const {
data: policyInfo,
isFetching: isFetchingPolicyInfo,
@ -109,28 +74,23 @@ export const OscapProfileInformation = ({
policyTitle: pol.title,
}),
);
}, [isSuccessPolicyInfo, dispatch, policyInfo]);
}, [isSuccessPolicyInfo]);
const oscapProfile = profileMetadata?.openscap as OpenScapProfile | undefined;
const oscapProfile = oscapProfileInfo?.openscap as OpenScapProfile;
return (
<>
{(isFetchingOscapData || isFetchingPolicyInfo) && <Spinner size='lg' />}
{hasCriticalError && (
<Content component={ContentVariants.p} className='pf-v6-u-color-200'>
Unable to load compliance information. Please try again.
</Content>
{(isFetchingOscapProfileInfo || isFetchingPolicyInfo) && (
<Spinner size='lg' />
)}
{shouldShowData && (
{isSuccessOscapProfileInfo && (
<>
<Content component={ContentVariants.dl} className='review-step-dl'>
<Content
component={ContentVariants.dt}
className='pf-v6-u-min-width'
>
{complianceType === 'compliance'
? 'Policy description'
: 'Profile description'}
Profile description
</Content>
<Content component={ContentVariants.dd}>
{oscapProfile?.profile_description}
@ -156,7 +116,7 @@ export const OscapProfileInformation = ({
<Content component={ContentVariants.dd}>
<CodeBlock>
<CodeBlockCode>
{(customizationData?.packages ?? []).join(', ')}
{(oscapProfileInfo?.packages ?? []).join(', ')}
</CodeBlockCode>
</CodeBlock>
</Content>
@ -169,7 +129,7 @@ export const OscapProfileInformation = ({
<Content component={ContentVariants.dd}>
<CodeBlock>
<CodeBlockCode>
{customizationData?.kernel?.append}
{oscapProfileInfo?.kernel?.append}
</CodeBlockCode>
</CodeBlock>
</Content>
@ -182,7 +142,7 @@ export const OscapProfileInformation = ({
<Content component={ContentVariants.dd}>
<CodeBlock>
<CodeBlockCode>
{(customizationData?.services?.enabled ?? []).join(' ')}
{(oscapProfileInfo?.services?.enabled ?? []).join(' ')}
</CodeBlockCode>
</CodeBlock>
</Content>
@ -195,8 +155,8 @@ export const OscapProfileInformation = ({
<Content component={ContentVariants.dd}>
<CodeBlock>
<CodeBlockCode>
{(customizationData?.services?.disabled ?? [])
.concat(customizationData?.services?.masked ?? [])
{(oscapProfileInfo?.services?.disabled ?? [])
.concat(oscapProfileInfo?.services?.masked ?? [])
.join(' ')}
</CodeBlockCode>
</CodeBlock>

View file

@ -10,15 +10,15 @@ import {
import { useSelectorHandlers } from './useSelectorHandlers';
import {
useGetComplianceCustomizationsQuery,
useLazyGetComplianceCustomizationsQuery,
} from '../../../../../store/backendApi';
import {
PolicyRead,
usePoliciesQuery,
} from '../../../../../store/complianceApi';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import {
useGetOscapCustomizationsForPolicyQuery,
useLazyGetOscapCustomizationsForPolicyQuery,
} from '../../../../../store/imageBuilderApi';
import {
changeCompliance,
changeFileSystemConfigurationType,
@ -97,7 +97,7 @@ const PolicySelector = () => {
filter: `os_major_version=${majorVersion}`,
});
const { data: currentProfileData } = useGetComplianceCustomizationsQuery(
const { data: currentProfileData } = useGetOscapCustomizationsForPolicyQuery(
{
distribution: release,
policy: policyID!,
@ -105,7 +105,7 @@ const PolicySelector = () => {
{ skip: !policyID },
);
const [trigger] = useLazyGetComplianceCustomizationsQuery();
const [trigger] = useLazyGetOscapCustomizationsForPolicyQuery();
useEffect(() => {
if (!policies || policies.data === undefined) {

View file

@ -50,7 +50,6 @@ import {
Thead,
Tr,
} from '@patternfly/react-table';
import { orderBy } from 'lodash';
import { useDispatch } from 'react-redux';
import CustomHelperText from './components/CustomHelperText';
@ -67,6 +66,7 @@ import {
} from '../../../../constants';
import { useGetArchitecturesQuery } from '../../../../store/backendApi';
import {
ApiPackageSourcesResponse,
ApiRepositoryResponseRead,
ApiSearchRpmResponse,
useCreateRepositoryMutation,
@ -700,7 +700,7 @@ const Packages = () => {
);
}
let unpackedData: IBPackageWithRepositoryInfo[] =
const unpackedData: IBPackageWithRepositoryInfo[] =
combinedPackageData.flatMap((item) => {
// Spread modules into separate rows by application stream
if (item.sources) {
@ -724,16 +724,13 @@ const Packages = () => {
});
// group by name, but sort by application stream in descending order
unpackedData = orderBy(
unpackedData,
[
'name',
(pkg) => pkg.stream || '',
(pkg) => pkg.repository || '',
(pkg) => pkg.module_name || '',
],
['asc', 'desc', 'asc', 'asc'],
);
unpackedData.sort((a, b) => {
if (a.name === b.name) {
return (b.stream ?? '').localeCompare(a.stream ?? '');
} else {
return a.name.localeCompare(b.name);
}
});
if (toggleSelected === 'toggle-available') {
if (activeTabKey === Repos.INCLUDED) {
@ -869,6 +866,8 @@ const Packages = () => {
dispatch(addPackage(pkg));
if (pkg.type === 'module') {
setActiveStream(pkg.stream || '');
setActiveSortIndex(2);
setPage(1);
dispatch(
addModule({
name: pkg.module_name || '',
@ -994,18 +993,7 @@ const Packages = () => {
}
};
const getPackageUniqueKey = (pkg: IBPackageWithRepositoryInfo): string => {
try {
if (!pkg || !pkg.name) {
return `invalid_${Date.now()}`;
}
return `${pkg.name}_${pkg.stream || 'none'}_${pkg.module_name || 'none'}_${pkg.repository || 'unknown'}`;
} catch {
return `error_${Date.now()}`;
}
};
const initialExpandedPkgs: string[] = [];
const initialExpandedPkgs: IBPackageWithRepositoryInfo[] = [];
const [expandedPkgs, setExpandedPkgs] = useState(initialExpandedPkgs);
const setPkgExpanded = (
@ -1013,13 +1001,12 @@ const Packages = () => {
isExpanding: boolean,
) =>
setExpandedPkgs((prevExpanded) => {
const pkgKey = getPackageUniqueKey(pkg);
const otherExpandedPkgs = prevExpanded.filter((key) => key !== pkgKey);
return isExpanding ? [...otherExpandedPkgs, pkgKey] : otherExpandedPkgs;
const otherExpandedPkgs = prevExpanded.filter((p) => p.name !== pkg.name);
return isExpanding ? [...otherExpandedPkgs, pkg] : otherExpandedPkgs;
});
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
expandedPkgs.includes(getPackageUniqueKey(pkg));
expandedPkgs.includes(pkg);
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
@ -1043,37 +1030,51 @@ const Packages = () => {
'asc' | 'desc'
>('asc');
const sortedPackages = useMemo(() => {
if (!transformedPackages || !Array.isArray(transformedPackages)) {
return [];
}
const getSortableRowValues = (
pkg: IBPackageWithRepositoryInfo,
): (string | number | ApiPackageSourcesResponse[] | undefined)[] => {
return [pkg.name, pkg.summary, pkg.stream, pkg.end_date, pkg.repository];
};
return orderBy(
transformedPackages,
[
// Active stream packages first (if activeStream is set)
(pkg) => (activeStream && pkg.stream === activeStream ? 0 : 1),
// Then by name
'name',
// Then by stream version (descending)
(pkg) => {
if (!pkg.stream) return '';
const parts = pkg.stream
.split('.')
.map((part) => parseInt(part, 10) || 0);
// Convert to string with zero-padding for proper sorting
return parts.map((p) => p.toString().padStart(10, '0')).join('.');
},
// Then by end date (nulls last)
(pkg) => pkg.end_date || '9999-12-31',
// Then by repository
(pkg) => pkg.repository || '',
// Finally by module name
(pkg) => pkg.module_name || '',
],
['asc', 'asc', 'desc', 'asc', 'asc', 'asc'],
);
}, [transformedPackages, activeStream]);
let sortedPackages = transformedPackages;
sortedPackages = transformedPackages.sort((a, b) => {
const aValue = getSortableRowValues(a)[activeSortIndex];
const bValue = getSortableRowValues(b)[activeSortIndex];
if (typeof aValue === 'number') {
// Numeric sort
if (activeSortDirection === 'asc') {
return (aValue as number) - (bValue as number);
}
return (bValue as number) - (aValue as number);
}
// String sort
// if active stream is set, sort it to the top
if (aValue === activeStream) {
return -1;
}
if (bValue === activeStream) {
return 1;
}
if (activeSortDirection === 'asc') {
// handle packages with undefined stream
if (!aValue) {
return -1;
}
if (!bValue) {
return 1;
}
return (aValue as string).localeCompare(bValue as string);
} else {
// handle packages with undefined stream
if (!aValue) {
return 1;
}
if (!bValue) {
return -1;
}
return (bValue as string).localeCompare(aValue as string);
}
});
const getSortParams = (columnIndex: number) => ({
sortBy: {
@ -1099,14 +1100,14 @@ const Packages = () => {
(module) => module.name === pkg.name,
);
isSelected =
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
!isModuleWithSameName;
packages.some((p) => p.name === pkg.name) && !isModuleWithSameName;
}
if (pkg.type === 'module') {
// the package is selected if its module stream matches one in enabled_modules
// the package is selected if it's added to the packages state
// and its module stream matches one in enabled_modules
isSelected =
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
packages.some((p) => p.name === pkg.name) &&
modules.some(
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
);
@ -1207,7 +1208,7 @@ const Packages = () => {
.slice(computeStart(), computeEnd())
.map((grp, rowIndex) => (
<Tbody
key={`${grp.name}-${grp.repository || 'default'}`}
key={`${grp.name}-${rowIndex}`}
isExpanded={isGroupExpanded(grp.name)}
>
<Tr data-testid='package-row'>
@ -1307,7 +1308,7 @@ const Packages = () => {
.slice(computeStart(), computeEnd())
.map((pkg, rowIndex) => (
<Tbody
key={`${pkg.name}-${pkg.stream || 'default'}-${pkg.module_name || pkg.name}`}
key={`${pkg.name}-${rowIndex}`}
isExpanded={isPkgExpanded(pkg)}
>
<Tr data-testid='package-row'>

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
ClipboardCopy,
@ -16,7 +16,6 @@ import ActivationKeysList from './components/ActivationKeysList';
import Registration from './components/Registration';
import SatelliteRegistration from './components/SatelliteRegistration';
import { useGetUser } from '../../../../Hooks';
import { useAppSelector } from '../../../../store/hooks';
import {
selectActivationKey,
@ -25,7 +24,18 @@ import {
const RegistrationStep = () => {
const { auth } = useChrome();
const { orgId } = useGetUser(auth);
const [orgId, setOrgId] = useState<string | undefined>(undefined);
useEffect(() => {
(async () => {
const userData = await auth.getUser();
const id = userData?.identity?.internal?.org_id;
setOrgId(id);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const activationKey = useAppSelector(selectActivationKey);
const registrationType = useAppSelector(selectRegistrationType);

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Button,
@ -14,12 +14,12 @@ import {
Spinner,
} from '@patternfly/react-core';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
import {
useComposeBPWithNotification as useComposeBlueprintMutation,
useCreateBPWithNotification as useCreateBlueprintMutation,
useGetUser,
} from '../../../../../Hooks';
import { setBlueprintId } from '../../../../../store/BlueprintSlice';
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
@ -44,8 +44,19 @@ export const CreateSaveAndBuildBtn = ({
setIsOpen,
isDisabled,
}: CreateDropdownProps) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth, isBeta } = useChrome();
const { userData } = useGetUser(auth);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const packages = useAppSelector(selectPackages);
@ -102,7 +113,17 @@ export const CreateSaveButton = ({
isDisabled,
}: CreateDropdownProps) => {
const { analytics, auth, isBeta } = useChrome();
const { userData } = useGetUser(auth);
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const packages = useAppSelector(selectPackages);

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
DropdownItem,
@ -9,11 +9,11 @@ import {
Spinner,
} from '@patternfly/react-core';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
import {
useComposeBPWithNotification as useComposeBlueprintMutation,
useGetUser,
useUpdateBPWithNotification as useUpdateBlueprintMutation,
} from '../../../../../Hooks';
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
@ -37,8 +37,19 @@ export const EditSaveAndBuildBtn = ({
blueprintId,
isDisabled,
}: EditDropdownProps) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth, isBeta } = useChrome();
const { userData } = useGetUser(auth);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { trigger: buildBlueprint } = useComposeBlueprintMutation();
const packages = useAppSelector(selectPackages);
@ -94,8 +105,19 @@ export const EditSaveButton = ({
blueprintId,
isDisabled,
}: EditDropdownProps) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth, isBeta } = useChrome();
const { userData } = useGetUser(auth);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const packages = useAppSelector(selectPackages);

View file

@ -18,7 +18,6 @@ import { EditSaveAndBuildBtn, EditSaveButton } from './EditDropdown';
import {
useCreateBPWithNotification as useCreateBlueprintMutation,
useGetUser,
useUpdateBPWithNotification as useUpdateBlueprintMutation,
} from '../../../../../Hooks';
import { resolveRelPath } from '../../../../../Utilities/path';
@ -34,7 +33,6 @@ const ReviewWizardFooter = () => {
const { isSuccess: isUpdateSuccess, reset: resetUpdate } =
useUpdateBlueprintMutation({ fixedCacheKey: 'updateBlueprintKey' });
const { auth } = useChrome();
const { orgId } = useGetUser(auth);
const { composeId } = useParams();
const [isOpen, setIsOpen] = useState(false);
const store = useStore();
@ -54,12 +52,14 @@ const ReviewWizardFooter = () => {
const getBlueprintPayload = async () => {
if (!process.env.IS_ON_PREMISE) {
const userData = await auth.getUser();
const orgId = userData?.identity?.internal?.org_id;
const requestBody = orgId && mapRequestFromState(store, orgId);
return requestBody;
}
// NOTE: This is fine for on prem because we save the org id
// to state through a form field in the registration step
// NOTE: This should be fine on-prem, we should
// be able to ignore the `org-id`
return mapRequestFromState(store, '');
};

View file

@ -25,7 +25,6 @@ import {
KernelList,
LocaleList,
OscapList,
RegisterAapList,
RegisterLaterList,
RegisterNowList,
RegisterSatelliteList,
@ -43,7 +42,6 @@ import isRhel from '../../../../../src/Utilities/isRhel';
import { targetOptions } from '../../../../constants';
import { useAppSelector } from '../../../../store/hooks';
import {
selectAapRegistration,
selectBlueprintDescription,
selectBlueprintName,
selectCompliancePolicyID,
@ -67,7 +65,6 @@ import { useHasSpecificTargetOnly } from '../../utilities/hasSpecificTargetOnly'
const Review = () => {
const { goToStepById } = useWizardContext();
const aapRegistration = useAppSelector(selectAapRegistration);
const blueprintName = useAppSelector(selectBlueprintName);
const blueprintDescription = useAppSelector(selectBlueprintDescription);
const distribution = useAppSelector(selectDistribution);
@ -86,7 +83,6 @@ const Review = () => {
const users = useAppSelector(selectUsers);
const kernel = useAppSelector(selectKernel);
const [isExpandedAap, setIsExpandedAap] = useState(true);
const [isExpandedImageOutput, setIsExpandedImageOutput] = useState(true);
const [isExpandedTargetEnvs, setIsExpandedTargetEnvs] = useState(true);
const [isExpandedFSC, setIsExpandedFSC] = useState(true);
@ -105,8 +101,6 @@ const Review = () => {
const [isExpandableFirstBoot, setIsExpandedFirstBoot] = useState(true);
const [isExpandedUsers, setIsExpandedUsers] = useState(true);
const onToggleAap = (isExpandedAap: boolean) =>
setIsExpandedAap(isExpandedAap);
const onToggleImageOutput = (isExpandedImageOutput: boolean) =>
setIsExpandedImageOutput(isExpandedImageOutput);
const onToggleTargetEnvs = (isExpandedTargetEnvs: boolean) =>
@ -505,21 +499,6 @@ const Review = () => {
<ServicesList />
</ExpandableSection>
)}
{aapRegistration.callbackUrl && (
<ExpandableSection
toggleContent={composeExpandable(
'Ansible Automation Platform',
'revisit-aap',
'wizard-aap',
)}
onToggle={(_event, isExpandableAap) => onToggleAap(isExpandableAap)}
isExpanded={isExpandedAap}
isIndented
data-testid='aap-expandable'
>
<RegisterAapList />
</ExpandableSection>
)}
{!process.env.IS_ON_PREMISE && (
<ExpandableSection
toggleContent={composeExpandable(

View file

@ -41,10 +41,6 @@ import { useAppSelector } from '../../../../store/hooks';
import { useGetSourceListQuery } from '../../../../store/provisioningApi';
import { useShowActivationKeyQuery } from '../../../../store/rhsmApi';
import {
selectAapCallbackUrl,
selectAapHostConfigKey,
selectAapTlsCertificateAuthority,
selectAapTlsConfirmation,
selectActivationKey,
selectArchitecture,
selectAwsAccountId,
@ -664,45 +660,6 @@ export const RegisterSatelliteList = () => {
);
};
export const RegisterAapList = () => {
const callbackUrl = useAppSelector(selectAapCallbackUrl);
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
const tlsCertificateAuthority = useAppSelector(
selectAapTlsCertificateAuthority,
);
const skipTlsVerification = useAppSelector(selectAapTlsConfirmation);
const getTlsStatus = () => {
if (skipTlsVerification) {
return 'Insecure (TLS verification skipped)';
}
return tlsCertificateAuthority ? 'Configured' : 'None';
};
return (
<Content>
<Content component={ContentVariants.dl} className='review-step-dl'>
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
Ansible Callback URL
</Content>
<Content component={ContentVariants.dd}>
{callbackUrl || 'None'}
</Content>
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
Host Config Key
</Content>
<Content component={ContentVariants.dd}>
{hostConfigKey || 'None'}
</Content>
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
TLS Certificate
</Content>
<Content component={ContentVariants.dd}>{getTlsStatus()}</Content>
</Content>
</Content>
);
};
export const RegisterNowList = () => {
const activationKey = useAppSelector(selectActivationKey);
const registrationType = useAppSelector(selectRegistrationType);

View file

@ -23,7 +23,6 @@ import {
CockpitUploadTypes,
} from '../../../store/cockpit/types';
import {
AapRegistration,
AwsUploadRequestOptions,
AzureUploadRequestOptions,
BlueprintExportResponse,
@ -50,11 +49,6 @@ import { ApiRepositoryImportResponseRead } from '../../../store/service/contentS
import {
ComplianceType,
initialState,
RegistrationType,
selectAapCallbackUrl,
selectAapHostConfigKey,
selectAapTlsCertificateAuthority,
selectAapTlsConfirmation,
selectActivationKey,
selectArchitecture,
selectAwsAccountId,
@ -211,9 +205,8 @@ function commonRequestToState(
snapshot_date = '';
}
// we need to check for the region for on-prem
const awsUploadOptions = aws?.upload_request
.options as AwsUploadRequestOptions & { region?: string | undefined };
.options as AwsUploadRequestOptions;
const gcpUploadOptions = gcp?.upload_request
.options as GcpUploadRequestOptions;
const azureUploadOptions = azure?.upload_request
@ -316,7 +309,6 @@ function commonRequestToState(
: 'manual') as AwsShareMethod,
source: { id: awsUploadOptions?.share_with_sources?.[0] },
sourceId: awsUploadOptions?.share_with_sources?.[0],
region: awsUploadOptions?.region,
},
snapshotting: {
useLatest: !snapshot_date && !request.image_requests[0]?.content_template,
@ -395,7 +387,14 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
baseUrl: request.customizations.subscription?.['base-url'] || '',
},
registration: {
registrationType: getRegistrationType(request),
registrationType:
request.customizations?.subscription && isRhel(request.distribution)
? request.customizations.subscription.rhc
? 'register-now-rhc'
: 'register-now-insights'
: getSatelliteCommand(request.customizations.files)
? 'register-satellite'
: 'register-later',
activationKey: isRhel(request.distribution)
? request.customizations.subscription?.['activation-key']
: undefined,
@ -404,15 +403,6 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
caCert: request.customizations.cacerts?.pem_certs[0],
},
},
aapRegistration: {
callbackUrl:
request.customizations?.aap_registration?.ansible_callback_url,
hostConfigKey: request.customizations?.aap_registration?.host_config_key,
tlsCertificateAuthority:
request.customizations?.aap_registration?.tls_certificate_authority,
skipTlsVerification:
request.customizations?.aap_registration?.skip_tls_verification,
},
...commonRequestToState(request),
};
};
@ -462,15 +452,6 @@ export const mapExportRequestToState = (
},
env: initialState.env,
registration: initialState.registration,
aapRegistration: {
callbackUrl:
request.customizations?.aap_registration?.ansible_callback_url,
hostConfigKey: request.customizations?.aap_registration?.host_config_key,
tlsCertificateAuthority:
request.customizations?.aap_registration?.tls_certificate_authority,
skipTlsVerification:
request.customizations?.aap_registration?.skip_tls_verification,
},
...commonRequestToState(blueprintResponse),
};
};
@ -480,24 +461,6 @@ const getFirstBootScript = (files?: File[]): string => {
return firstBootFile?.data ? atob(firstBootFile.data) : '';
};
const getAapRegistration = (state: RootState): AapRegistration | undefined => {
const callbackUrl = selectAapCallbackUrl(state);
const hostConfigKey = selectAapHostConfigKey(state);
const tlsCertificateAuthority = selectAapTlsCertificateAuthority(state);
const skipTlsVerification = selectAapTlsConfirmation(state);
if (!callbackUrl && !hostConfigKey && !tlsCertificateAuthority) {
return undefined;
}
return {
ansible_callback_url: callbackUrl || '',
host_config_key: hostConfigKey || '',
tls_certificate_authority: tlsCertificateAuthority || undefined,
skip_tls_verification: skipTlsVerification || undefined,
};
};
const getImageRequests = (
state: RootState,
): ImageRequest[] | CockpitImageRequest[] => {
@ -519,24 +482,6 @@ const getImageRequests = (
}));
};
const getRegistrationType = (request: BlueprintResponse): RegistrationType => {
const subscription = request.customizations.subscription;
const distribution = request.distribution;
const files = request.customizations.files;
if (subscription && isRhel(distribution)) {
if (subscription.rhc) {
return 'register-now-rhc';
} else {
return 'register-now-insights';
}
} else if (getSatelliteCommand(files)) {
return 'register-satellite';
} else {
return 'register-later';
}
};
const getSatelliteCommand = (files?: File[]): string => {
const satelliteCommandFile = files?.find(
(file) => file.path === SATELLITE_PATH,
@ -697,7 +642,6 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
pem_certs: [satCert],
}
: undefined,
aap_registration: getAapRegistration(state),
};
};

View file

@ -11,10 +11,6 @@ import { useAppSelector } from '../../../store/hooks';
import { BlueprintsResponse } from '../../../store/imageBuilderApi';
import { useShowActivationKeyQuery } from '../../../store/rhsmApi';
import {
selectAapCallbackUrl,
selectAapHostConfigKey,
selectAapTlsCertificateAuthority,
selectAapTlsConfirmation,
selectActivationKey,
selectBlueprintDescription,
selectBlueprintId,
@ -58,8 +54,6 @@ import {
isSshKeyValid,
isUserGroupValid,
isUserNameValid,
isValidUrl,
validateMultipleCertificates,
} from '../validators';
export type StepValidation = {
@ -211,62 +205,6 @@ export function useRegistrationValidation(): StepValidation {
return { errors: {}, disabledNext: false };
}
export function useAAPValidation(): StepValidation {
const errors: Record<string, string> = {};
const callbackUrl = useAppSelector(selectAapCallbackUrl);
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
const tlsCertificateAuthority = useAppSelector(
selectAapTlsCertificateAuthority,
);
const tlsConfirmation = useAppSelector(selectAapTlsConfirmation);
if (!callbackUrl && !hostConfigKey && !tlsCertificateAuthority) {
return { errors: {}, disabledNext: false };
}
if (!callbackUrl || callbackUrl.trim() === '') {
errors.callbackUrl = 'Ansible Callback URL is required';
} else if (!isValidUrl(callbackUrl)) {
errors.callbackUrl = 'Callback URL must be a valid URL';
}
if (!hostConfigKey || hostConfigKey.trim() === '') {
errors.hostConfigKey = 'Host Config Key is required';
}
if (tlsCertificateAuthority && tlsCertificateAuthority.trim() !== '') {
const validation = validateMultipleCertificates(tlsCertificateAuthority);
if (validation.errors.length > 0) {
errors.certificate = validation.errors.join(' ');
} else if (validation.validCertificates.length === 0) {
errors.certificate = 'No valid certificates found in the input.';
}
}
if (callbackUrl && callbackUrl.trim() !== '') {
const isHttpsUrl = callbackUrl.toLowerCase().startsWith('https://');
// If URL is HTTP, require TLS certificate
if (
!isHttpsUrl &&
(!tlsCertificateAuthority || tlsCertificateAuthority.trim() === '')
) {
errors.certificate = 'HTTP URL requires a custom TLS certificate';
return { errors, disabledNext: true };
}
// For HTTPS URL, if the TLS confirmation is not checked, require certificate
if (
!tlsConfirmation &&
(!tlsCertificateAuthority || tlsCertificateAuthority.trim() === '')
) {
errors.certificate =
'HTTPS URL requires either a custom TLS certificate or confirmation that no custom certificate is needed';
}
}
return { errors, disabledNext: Object.keys(errors).length > 0 };
}
export function useFilesystemValidation(): StepValidation {
const mode = useAppSelector(selectFileSystemConfigurationType);
const partitions = useAppSelector(selectPartitions);

View file

@ -138,85 +138,3 @@ export const isServiceValid = (service: string) => {
/[a-zA-Z]+/.test(service) // contains at least one letter
);
};
export const isValidUrl = (url: string): boolean => {
try {
const parsedUrl = new URL(url);
const isHttpOrHttps = ['http:', 'https:'].includes(parsedUrl.protocol);
const hostname = parsedUrl.hostname;
const hasValidDomain =
/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:\.[a-zA-Z]{2,})*$/.test(hostname);
return isHttpOrHttps && hasValidDomain;
} catch {
return false;
}
};
export const isValidCA = (ca: string) => {
if (!ca || typeof ca !== 'string') return false;
const trimmed = ca.trim();
const pemPattern =
/^-----BEGIN CERTIFICATE-----[\r\n]+([\s\S]*?)[\r\n]+-----END CERTIFICATE-----$/;
if (!pemPattern.test(trimmed)) {
return false;
}
const match = trimmed.match(pemPattern);
if (!match || !match[1]) {
return false;
}
const base64Content = match[1].replace(/[\r\n\s]/g, '');
const base64Pattern = /^[A-Za-z0-9+/]+(=*)$/;
return base64Pattern.test(base64Content) && base64Content.length > 0;
};
export const parseMultipleCertificates = (input: string): string[] => {
if (!input || typeof input !== 'string') return [];
const blockPattern =
/-----BEGIN CERTIFICATE-----[\s\S]*?(?=-----BEGIN CERTIFICATE-----|$)/g;
const matches = input.match(blockPattern);
return matches ? matches.map((m) => m.trim()) : [];
};
export const validateMultipleCertificates = (
input: string,
): {
certificates: string[];
validCertificates: string[];
invalidCertificates: string[];
errors: string[];
} => {
const certificates = parseMultipleCertificates(input);
const validCertificates: string[] = [];
const invalidCertificates: string[] = [];
const errors: string[] = [];
if (certificates.length === 0 && input.trim() !== '') {
errors.push(
'No valid certificate format found. Certificates must be in PEM/DER/CER format.',
);
return { certificates, validCertificates, invalidCertificates, errors };
}
certificates.forEach((cert, index) => {
if (isValidCA(cert)) {
validCertificates.push(cert);
} else {
invalidCertificates.push(cert);
errors.push(
`Certificate ${index + 1} is not valid. Must be in PEM/DER/CER format.`,
);
}
});
return { certificates, validCertificates, invalidCertificates, errors };
};

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
Alert,
@ -13,11 +13,11 @@ import {
} from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import ClonesTable from './ClonesTable';
import { AMPLITUDE_MODULE_NAME } from '../../constants';
import { useGetUser } from '../../Hooks';
import { useGetComposeStatusQuery } from '../../store/backendApi';
import { extractProvisioningList } from '../../store/helpers';
import {
@ -119,7 +119,7 @@ const AwsSourceName = ({ id }: AwsSourceNamePropTypes) => {
return <SourceNotFoundPopover />;
};
export const parseGcpSharedWith = (
const parseGcpSharedWith = (
sharedWith: GcpUploadRequestOptions['share_with_accounts'],
) => {
if (sharedWith) {
@ -134,9 +134,19 @@ type AwsDetailsPropTypes = {
export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => {
const options = compose.request.image_requests[0].upload_request.options;
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
const { userData } = useGetUser(auth);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!isAwsUploadRequestOptions(options)) {
throw TypeError(

View file

@ -25,7 +25,7 @@ import {
Tr,
} from '@patternfly/react-table';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { useFlag } from '@unleash/proxy-client-react';
import { ChromeUser } from '@redhat-cloud-services/types';
import { useDispatch } from 'react-redux';
import { NavigateFunction, useNavigate } from 'react-router-dom';
@ -58,7 +58,6 @@ import {
SEARCH_INPUT,
STATUS_POLLING_INTERVAL,
} from '../../constants';
import { useGetUser } from '../../Hooks';
import {
useGetBlueprintComposesQuery,
useGetBlueprintsQuery,
@ -88,12 +87,11 @@ import {
timestampToDisplayString,
timestampToDisplayStringDetailed,
} from '../../Utilities/time';
import { AzureLaunchModal } from '../Launch/AzureLaunchModal';
import { OciLaunchModal } from '../Launch/OciLaunchModal';
const ImagesTable = () => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const blueprintSearchInput =
@ -106,7 +104,16 @@ const ImagesTable = () => {
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
const { analytics, auth } = useChrome();
const { userData } = useGetUser(auth);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const searchParamsGetBlueprints: GetBlueprintsApiArg = {
limit: blueprintsLimit,
@ -375,14 +382,8 @@ type AzureRowPropTypes = {
};
const AzureRow = ({ compose, rowIndex }: AzureRowPropTypes) => {
const launchEofFlag = useFlag('image-builder.launcheof');
const details = <AzureDetails compose={compose} />;
const instance = launchEofFlag ? (
<AzureLaunchModal compose={compose} />
) : (
<CloudInstance compose={compose} />
);
const instance = <CloudInstance compose={compose} />;
const status = <CloudStatus compose={compose} />;
return (
@ -402,18 +403,13 @@ type OciRowPropTypes = {
};
const OciRow = ({ compose, rowIndex }: OciRowPropTypes) => {
const launchEofFlag = useFlag('image-builder.launcheof');
const daysToExpiration = Math.floor(
computeHoursToExpiration(compose.created_at) / 24,
);
const isExpired = daysToExpiration >= OCI_STORAGE_EXPIRATION_TIME_IN_DAYS;
const details = <OciDetails compose={compose} />;
const instance = launchEofFlag ? (
<OciLaunchModal compose={compose} isExpired={isExpired} />
) : (
<OciInstance compose={compose} isExpired={isExpired} />
);
const instance = <OciInstance compose={compose} isExpired={isExpired} />;
const status = (
<ExpiringStatus
compose={compose}
@ -471,8 +467,18 @@ type AwsRowPropTypes = {
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
const navigate = useNavigate();
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
const { userData } = useGetUser(auth);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const target = <AwsTarget compose={compose} />;
const status = <CloudStatus compose={compose} />;
@ -547,8 +553,18 @@ const Row = ({
details,
instance,
}: RowPropTypes) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
const { userData } = useGetUser(auth);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [isExpanded, setIsExpanded] = useState(false);
const handleToggle = () => setIsExpanded(!isExpanded);

View file

@ -1,4 +1,4 @@
import React, { Suspense, useState } from 'react';
import React, { Suspense, useEffect, useState } from 'react';
import path from 'path';
@ -20,6 +20,7 @@ import {
} from '@patternfly/react-core/dist/esm/components/List/List';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { useLoadModule, useScalprum } from '@scalprum/react-core';
import cockpit from 'cockpit';
import { useNavigate } from 'react-router-dom';
@ -30,7 +31,6 @@ import {
MODAL_ANCHOR,
SEARCH_INPUT,
} from '../../constants';
import { useGetUser } from '../../Hooks';
import {
useGetBlueprintsQuery,
useGetComposeStatusQuery,
@ -54,10 +54,7 @@ import {
isOciUploadStatus,
} from '../../store/typeGuards';
import { resolveRelPath } from '../../Utilities/path';
import { useFlag } from '../../Utilities/useGetEnvironment';
import useProvisioningPermissions from '../../Utilities/useProvisioningPermissions';
import { AWSLaunchModal } from '../Launch/AWSLaunchModal';
import { GcpLaunchModal } from '../Launch/GcpLaunchModal';
type CloudInstancePropTypes = {
compose: ComposesResponseItem;
@ -100,12 +97,21 @@ const ProvisioningLink = ({
compose,
composeStatus,
}: ProvisioningLinkPropTypes) => {
const launchEofFlag = useFlag('image-builder.launcheof');
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
const { userData } = useGetUser(auth);
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [wizardOpen, setWizardOpen] = useState(false);
const [exposedScalprumModule, error] = useLoadModule(
{
scope: 'provisioning',
@ -176,7 +182,7 @@ const ProvisioningLink = ({
account_id: userData?.identity.internal?.account_id || 'Not found',
});
setIsModalOpen(true);
setWizardOpen(true);
}}
>
Launch
@ -196,10 +202,6 @@ const ProvisioningLink = ({
</Popover>
);
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
setIsModalOpen(!isModalOpen);
};
return (
<>
<Suspense fallback='loading...'>
@ -207,23 +209,7 @@ const ProvisioningLink = ({
compose.blueprint_version !== selectedBlueprintVersion
? buttonWithTooltip
: btn}
{launchEofFlag && isModalOpen && provider === 'aws' && (
<AWSLaunchModal
isOpen={isModalOpen}
handleModalToggle={handleModalToggle}
compose={compose}
composeStatus={composeStatus}
/>
)}
{launchEofFlag && isModalOpen && provider === 'gcp' && (
<GcpLaunchModal
isOpen={isModalOpen}
handleModalToggle={handleModalToggle}
compose={compose}
composeStatus={composeStatus}
/>
)}
{!launchEofFlag && isModalOpen && (
{wizardOpen && (
<Modal
isOpen
appendTo={appendTo}
@ -232,7 +218,7 @@ const ProvisioningLink = ({
>
<ProvisioningWizard
hasAccess={permissions[provider]}
onClose={() => setIsModalOpen(false)}
onClose={() => setWizardOpen(false)}
image={{
name: compose.image_name || compose.id,
id: compose.id,

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import './ImageBuildStatus.scss';
import {
@ -24,13 +24,13 @@ import {
PendingIcon,
} from '@patternfly/react-icons';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import {
AMPLITUDE_MODULE_NAME,
AWS_S3_EXPIRATION_TIME_IN_HOURS,
OCI_STORAGE_EXPIRATION_TIME_IN_DAYS,
} from '../../constants';
import { useGetUser } from '../../Hooks';
import { useGetComposeStatusQuery } from '../../store/backendApi';
import { CockpitComposesResponseItem } from '../../store/cockpit/types';
import {
@ -122,8 +122,18 @@ export const CloudStatus = ({ compose }: CloudStatusPropTypes) => {
const { data, isSuccess } = useGetComposeStatusQuery({
composeId: compose.id,
});
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
const { userData } = useGetUser(auth);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!isSuccess) {
return <Skeleton />;

View file

@ -1,104 +0,0 @@
import React from 'react';
import {
Button,
List,
ListComponent,
ListItem,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
ModalVariant,
OrderType,
} from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import {
ComposesResponseItem,
ComposeStatus,
} from '../../store/imageBuilderApi';
import { isAwsUploadRequestOptions } from '../../store/typeGuards';
type LaunchProps = {
isOpen: boolean;
handleModalToggle: (event: KeyboardEvent | React.MouseEvent) => void;
compose: ComposesResponseItem;
composeStatus: ComposeStatus | undefined;
};
export const AWSLaunchModal = ({
isOpen,
handleModalToggle,
compose,
composeStatus,
}: LaunchProps) => {
const options = compose.request.image_requests[0].upload_request.options;
if (!isAwsUploadRequestOptions(options)) {
throw TypeError(
`Error: options must be of type AwsUploadRequestOptions, not ${typeof options}.`,
);
}
const amiId =
composeStatus?.image_status.status === 'success' &&
composeStatus.image_status.upload_status?.options &&
'ami' in composeStatus.image_status.upload_status.options
? composeStatus.image_status.upload_status.options.ami
: '';
return (
<Modal
isOpen={isOpen}
onClose={handleModalToggle}
variant={ModalVariant.large}
aria-label='Open launch guide modal'
>
<ModalHeader
title={'Launch with Amazon Web Services'}
labelId='modal-title'
description={compose.image_name}
/>
<ModalBody id='modal-box-body-basic'>
<List component={ListComponent.ol} type={OrderType.number}>
<ListItem>
Navigate to the{' '}
<Button
component='a'
target='_blank'
variant='link'
icon={<ExternalLinkAltIcon />}
iconPosition='right'
href={`https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#ImageDetails:imageId=${amiId}`}
className='pf-v6-u-pl-0'
>
Images detail page
</Button>{' '}
located on your AWS console.
</ListItem>
<ListItem>
Copy the image to make it a permanent copy in your account.
<br />
Shared with Account{' '}
<span className='pf-v6-u-font-weight-bold'>
{options.share_with_accounts?.[0]}
</span>
<br />
AMI ID: <span className='pf-v6-u-font-weight-bold'>{amiId}</span>
</ListItem>
<ListItem>Launch image as an instance.</ListItem>
<ListItem>
Connect to it via SSH using the following username:{' '}
<span className='pf-v6-u-font-weight-bold'>ec2-user</span>
</ListItem>
</List>
</ModalBody>
<ModalFooter>
<Button key='close' variant='primary' onClick={handleModalToggle}>
Close
</Button>
</ModalFooter>
</Modal>
);
};

View file

@ -1,116 +0,0 @@
import React, { Fragment, useState } from 'react';
import {
Button,
List,
ListComponent,
ListItem,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
ModalVariant,
OrderType,
Skeleton,
} from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import {
ComposesResponseItem,
useGetComposeStatusQuery,
} from '../../store/imageBuilderApi';
import { isAzureUploadStatus } from '../../store/typeGuards';
type LaunchProps = {
compose: ComposesResponseItem;
};
export const AzureLaunchModal = ({ compose }: LaunchProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const { data, isSuccess, isFetching } = useGetComposeStatusQuery({
composeId: compose.id,
});
if (!isSuccess) {
return <Skeleton />;
}
const options = data?.image_status.upload_status?.options;
if (options && !isAzureUploadStatus(options)) {
throw TypeError(
`Error: options must be of type AzureUploadStatus, not ${typeof options}.`,
);
}
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
setIsModalOpen(!isModalOpen);
};
return (
<Fragment>
<Button
variant='link'
isInline
isDisabled={data?.image_status.status !== 'success'}
onClick={handleModalToggle}
>
Launch
</Button>
<Modal
isOpen={isModalOpen}
onClose={handleModalToggle}
variant={ModalVariant.large}
aria-label='Open launch guide wizard'
>
<ModalHeader
title={'Launch with Microsoft Azure'}
labelId='modal-title'
description={compose.image_name}
/>
<ModalBody id='modal-box-body-basic'>
<List component={ListComponent.ol} type={OrderType.number}>
<ListItem>
Locate{' '}
{!isFetching && (
<span className='pf-v6-u-font-weight-bold'>
{options?.image_name}{' '}
</span>
)}
{isFetching && <Skeleton />}
in the{' '}
<Button
component='a'
target='_blank'
variant='link'
icon={<ExternalLinkAltIcon />}
iconPosition='right'
href={`https://portal.azure.com/#view/Microsoft_Azure_ComputeHub/ComputeHubMenuBlade/~/imagesBrowse`}
className='pf-v6-u-pl-0'
>
Azure console
</Button>
.
</ListItem>
<ListItem>
Create a Virtual Machine (VM) by using the image.
<br />
Note: Review the{' '}
<span className='pf-v6-u-font-weight-bold'>
Availability Zone
</span>{' '}
and the <span className='pf-v6-u-font-weight-bold'>Size</span> to
meet your requirements. Adjust these settings as needed.
</ListItem>
</List>
</ModalBody>
<ModalFooter>
<Button key='close' variant='primary' onClick={handleModalToggle}>
Close
</Button>
</ModalFooter>
</Modal>
</Fragment>
);
};

View file

@ -1,169 +0,0 @@
import React, { useState } from 'react';
import {
Button,
ClipboardCopy,
ClipboardCopyVariant,
List,
ListComponent,
ListItem,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
ModalVariant,
OrderType,
TextInput,
} from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { generateDefaultName } from './useGenerateDefaultName';
import {
ComposesResponseItem,
ComposeStatus,
} from '../../store/imageBuilderApi';
import {
isGcpUploadRequestOptions,
isGcpUploadStatus,
} from '../../store/typeGuards';
import { parseGcpSharedWith } from '../ImagesTable/ImageDetails';
type LaunchProps = {
isOpen: boolean;
handleModalToggle: (event: KeyboardEvent | React.MouseEvent) => void;
compose: ComposesResponseItem;
composeStatus: ComposeStatus | undefined;
};
export const GcpLaunchModal = ({
isOpen,
handleModalToggle,
compose,
composeStatus,
}: LaunchProps) => {
const [customerProjectId, setCustomerProjectId] = useState('');
const statusOptions = composeStatus?.image_status.upload_status?.options;
const composeOptions =
compose.request.image_requests[0].upload_request.options;
if (
(statusOptions && !isGcpUploadStatus(statusOptions)) ||
!isGcpUploadRequestOptions(composeOptions)
) {
throw TypeError(
`Error: options must be of type GcpUploadRequestOptions, not ${typeof statusOptions}.`,
);
}
const imageName = statusOptions?.image_name;
const projectId = statusOptions?.project_id;
if (!imageName || !projectId) {
throw TypeError(
`Error: Image name not found, unable to generate a command to copy ${typeof statusOptions}.`,
);
}
const uniqueImageName = generateDefaultName(imageName);
const authorizeString =
composeOptions.share_with_accounts &&
composeOptions.share_with_accounts.length === 1
? `Authorize gcloud CLI to the following
account: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.`
: composeOptions.share_with_accounts
? `Authorize gcloud CLI to use one of the following
accounts: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.`
: 'Authorize gcloud CLI to use the account that the image is shared with.';
const installationCommand = `sudo dnf install google-cloud-cli`;
const createImage = `gcloud compute images create ${uniqueImageName} --source-image=${imageName} --source-image-project=${projectId} --project=${
customerProjectId || '<your_project_id>'
}`;
const createInstance = `gcloud compute instances create ${uniqueImageName} --image=${uniqueImageName} --project=${
customerProjectId || '<your_project_id>'
}`;
return (
<Modal
isOpen={isOpen}
onClose={handleModalToggle}
variant={ModalVariant.large}
aria-label='Open launch guide modal'
>
<ModalHeader
title={'Launch with Google Cloud Platform'}
labelId='modal-title'
description={compose.image_name}
/>
<ModalBody id='modal-box-body-basic'>
<List component={ListComponent.ol} type={OrderType.number}>
<ListItem>
Install the gcloud CLI. See the{' '}
<Button
component='a'
target='_blank'
variant='link'
icon={<ExternalLinkAltIcon />}
iconPosition='right'
href={`https://cloud.google.com/sdk/docs/install`}
className='pf-v6-u-pl-0'
>
Install gcloud CLI
</Button>
documentation.
<ClipboardCopy isReadOnly hoverTip='Copy' clickTip='Copied'>
{installationCommand}
</ClipboardCopy>
</ListItem>
<ListItem>{authorizeString}</ListItem>
<ListItem>
Enter your GCP project ID, and run the command to create the image
in your project.
<TextInput
className='pf-v6-u-mt-sm pf-v6-u-mb-md'
value={customerProjectId}
type='text'
onChange={(_event, value) => setCustomerProjectId(value)}
aria-label='Project ID input'
placeholder='Project ID'
/>
<ClipboardCopy
isReadOnly
hoverTip='Copy'
clickTip='Copied'
variant={ClipboardCopyVariant.expansion}
>
{createImage}
</ClipboardCopy>
</ListItem>
<ListItem>
Create an instance of your image by either accessing the{' '}
<Button
component='a'
target='_blank'
variant='link'
icon={<ExternalLinkAltIcon />}
iconPosition='right'
href={`https://console.cloud.google.com/compute/images`}
className='pf-v6-u-pl-0'
>
GCP console
</Button>{' '}
or by running the following command:
<ClipboardCopy
isReadOnly
hoverTip='Copy'
clickTip='Copied'
variant={ClipboardCopyVariant.expansion}
>
{createInstance}
</ClipboardCopy>
</ListItem>
</List>
</ModalBody>
<ModalFooter>
<Button key='close' variant='primary' onClick={handleModalToggle}>
Close
</Button>
</ModalFooter>
</Modal>
);
};

View file

@ -1,139 +0,0 @@
import React, { Fragment, useState } from 'react';
import {
Button,
ClipboardCopy,
ClipboardCopyVariant,
List,
ListComponent,
ListItem,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
ModalVariant,
OrderType,
Skeleton,
} from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { useNavigate } from 'react-router-dom';
import {
ComposesResponseItem,
useGetComposeStatusQuery,
} from '../../store/imageBuilderApi';
import { isOciUploadStatus } from '../../store/typeGuards';
import { resolveRelPath } from '../../Utilities/path';
type LaunchProps = {
isExpired: boolean;
compose: ComposesResponseItem;
};
export const OciLaunchModal = ({ isExpired, compose }: LaunchProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const { data, isSuccess, isFetching } = useGetComposeStatusQuery({
composeId: compose.id,
});
const navigate = useNavigate();
if (!isSuccess) {
return <Skeleton />;
}
const options = data?.image_status.upload_status?.options;
if (options && !isOciUploadStatus(options)) {
throw TypeError(
`Error: options must be of type OciUploadStatus, not ${typeof options}.`,
);
}
if (isExpired) {
return (
<Button
component='a'
target='_blank'
variant='link'
onClick={() => navigate(resolveRelPath(`imagewizard/${compose.id}`))}
isInline
>
Recreate image
</Button>
);
}
const handleModalToggle = () => {
setIsModalOpen(!isModalOpen);
};
return (
<Fragment>
<Button
variant='link'
isInline
isDisabled={data?.image_status.status !== 'success'}
onClick={handleModalToggle}
>
Image link
</Button>
<Modal
isOpen={isModalOpen}
onClose={handleModalToggle}
variant={ModalVariant.large}
aria-label='Open launch guide modal'
>
<ModalHeader
title={'Launch with Oracle Cloud Infrastructure'}
labelId='modal-title'
description={compose.image_name}
/>
<ModalBody id='modal-box-body-basic'>
<List component={ListComponent.ol} type={OrderType.number}>
<ListItem>
Navigate to the{' '}
<Button
component='a'
target='_blank'
variant='link'
icon={<ExternalLinkAltIcon />}
iconPosition='right'
href={`https://cloud.oracle.com/compute/images`}
className='pf-v6-u-pl-0'
>
Oracle Cloud&apos;s Custom Images
</Button>{' '}
page.
</ListItem>
<ListItem>
Select{' '}
<span className='pf-v6-u-font-weight-bold'>Import image</span>,
and enter the Object Storage URL of the image.
{!isFetching && (
<ClipboardCopy
isReadOnly
isExpanded
hoverTip='Copy'
clickTip='Copied'
variant={ClipboardCopyVariant.expansion}
>
{options?.url || ''}
</ClipboardCopy>
)}
{isFetching && <Skeleton />}
</ListItem>
<ListItem>
After the image is available, click on{' '}
<span className='pf-v6-u-font-weight-bold'>Create instance</span>.
</ListItem>
</List>
</ModalBody>
<ModalFooter>
<Button key='close' variant='primary' onClick={handleModalToggle}>
Close
</Button>
</ModalFooter>
</Modal>
</Fragment>
);
};

View file

@ -1,34 +0,0 @@
export const generateDefaultName = (imageName: string) => {
const date = new Date();
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear().toString();
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const dateTimeString = `${month}${day}${year}-${hours}${minutes}`;
// gcloud images are valid in the form of: (?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)
let newBlueprintName = imageName
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-{2,}/g, '-')
.replace(/^-+|-+$/g, '');
if (!/^[a-z]/.test(newBlueprintName)) {
newBlueprintName = 'i' + newBlueprintName;
}
const maxLength = 63;
const uniquePartLength = dateTimeString.length + 1;
const baseNameMaxLength = maxLength - uniquePartLength;
if (newBlueprintName.length > baseNameMaxLength) {
newBlueprintName = newBlueprintName.substring(0, baseNameMaxLength);
}
while (newBlueprintName.endsWith('-')) {
newBlueprintName = newBlueprintName.slice(0, -1);
}
return `${newBlueprintName}-${dateTimeString}`;
};

View file

@ -156,7 +156,7 @@ const RegionsSelect = ({ composeId, handleClose }: RegionsSelectPropTypes) => {
<MenuToggle
variant='typeahead'
onClick={handleToggle}
ref={toggleRef}
innerRef={toggleRef}
isExpanded={isOpen}
>
<TextInputGroup isPlain>

View file

@ -4,4 +4,3 @@ export { useDeleteBPWithNotification } from './MutationNotifications/useDeleteBP
export { useFixupBPWithNotification } from './MutationNotifications/useFixupBPWithNotification';
export { useComposeBPWithNotification } from './MutationNotifications/useComposeBPWithNotification';
export { useCloneComposeWithNotification } from './MutationNotifications/useCloneComposeWithNotification';
export { useGetUser } from './useGetUser';

View file

@ -1,24 +0,0 @@
import { useEffect, useState } from 'react';
import { ChromeUser } from '@redhat-cloud-services/types';
export const useGetUser = (auth: { getUser(): Promise<void | ChromeUser> }) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const [orgId, setOrgId] = useState<string | undefined>(undefined);
useEffect(() => {
(async () => {
if (!process.env.IS_ON_PREMISE) {
const data = await auth.getUser();
const id = data?.identity.internal?.org_id;
setUserData(data);
setOrgId(id);
}
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { userData, orgId };
};

View file

@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.10.5'
const PACKAGE_VERSION = '2.10.4'
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View file

@ -43,12 +43,6 @@ export const useLazyGetOscapCustomizationsQuery = process.env.IS_ON_PREMISE
? cockpitQueries.useLazyGetOscapCustomizationsQuery
: serviceQueries.useLazyGetOscapCustomizationsQuery;
export const useGetComplianceCustomizationsQuery =
serviceQueries.useGetOscapCustomizationsForPolicyQuery;
export const useLazyGetComplianceCustomizationsQuery =
serviceQueries.useLazyGetOscapCustomizationsForPolicyQuery;
export const useComposeBlueprintMutation = process.env.IS_ON_PREMISE
? cockpitQueries.useComposeBlueprintMutation
: serviceQueries.useComposeBlueprintMutation;

View file

@ -44,8 +44,7 @@ export type RegistrationType =
| 'register-now'
| 'register-now-insights'
| 'register-now-rhc'
| 'register-satellite'
| 'register-aap';
| 'register-satellite';
export type ComplianceType = 'openscap' | 'compliance';
@ -90,12 +89,6 @@ export type wizardState = {
architecture: ImageRequest['architecture'];
distribution: Distributions;
imageTypes: ImageTypes[];
aapRegistration: {
callbackUrl: string | undefined;
hostConfigKey: string | undefined;
tlsCertificateAuthority: string | undefined;
skipTlsVerification: boolean | undefined;
};
aws: {
accountId: string;
shareMethod: AwsShareMethod;
@ -196,12 +189,6 @@ export const initialState: wizardState = {
architecture: X86_64,
distribution: RHEL_10,
imageTypes: [],
aapRegistration: {
callbackUrl: undefined,
hostConfigKey: undefined,
tlsCertificateAuthority: undefined,
skipTlsVerification: undefined,
},
aws: {
accountId: '',
shareMethod: 'sources',
@ -389,26 +376,6 @@ export const selectSatelliteCaCertificate = (state: RootState) => {
return state.wizard.registration.satelliteRegistration.caCert;
};
export const selectAapRegistration = (state: RootState) => {
return state.wizard.aapRegistration;
};
export const selectAapCallbackUrl = (state: RootState) => {
return state.wizard.aapRegistration?.callbackUrl;
};
export const selectAapHostConfigKey = (state: RootState) => {
return state.wizard.aapRegistration?.hostConfigKey;
};
export const selectAapTlsCertificateAuthority = (state: RootState) => {
return state.wizard.aapRegistration?.tlsCertificateAuthority;
};
export const selectAapTlsConfirmation = (state: RootState) => {
return state.wizard.aapRegistration?.skipTlsVerification;
};
export const selectComplianceProfileID = (state: RootState) => {
return state.wizard.compliance.profileID;
};
@ -660,22 +627,6 @@ export const wizardSlice = createSlice({
changeSatelliteCaCertificate: (state, action: PayloadAction<string>) => {
state.registration.satelliteRegistration.caCert = action.payload;
},
changeAapCallbackUrl: (state, action: PayloadAction<string>) => {
state.aapRegistration.callbackUrl = action.payload;
},
changeAapHostConfigKey: (state, action: PayloadAction<string>) => {
state.aapRegistration.hostConfigKey = action.payload;
},
changeAapTlsCertificateAuthority: (
state,
action: PayloadAction<string>,
) => {
state.aapRegistration.tlsCertificateAuthority = action.payload;
},
changeAapTlsConfirmation: (state, action: PayloadAction<boolean>) => {
state.aapRegistration.skipTlsVerification = action.payload;
},
changeActivationKey: (
state,
action: PayloadAction<ActivationKeys['name']>,
@ -1279,10 +1230,6 @@ export const {
changeTimezone,
changeSatelliteRegistrationCommand,
changeSatelliteCaCertificate,
changeAapCallbackUrl,
changeAapHostConfigKey,
changeAapTlsCertificateAuthority,
changeAapTlsConfirmation,
addNtpServer,
removeNtpServer,
changeHostname,

View file

@ -714,9 +714,6 @@ describe('Import modal', () => {
),
);
// AAP
await clickNext();
// Firstboot
await clickNext();
expect(

View file

@ -24,8 +24,6 @@ vi.mock('@unleash/proxy-client-react', () => ({
switch (flag) {
case 'image-builder.compliance.enabled':
return true;
case 'image-builder.aap.enabled':
return true;
default:
return false;
}

View file

@ -332,30 +332,4 @@ describe('OpenSCAP edit mode', () => {
user.click(selectedBtn);
await screen.findByText('neovim');
});
test('customized policy shows only non-removed rules', async () => {
const { oscapCustomizations, oscapCustomizationsPolicy } = await import(
'../../../../fixtures/oscap'
);
const profileId = 'xccdf_org.ssgproject.content_profile_cis_workstation_l1';
const normalProfile = oscapCustomizations(profileId);
expect(normalProfile.packages).toEqual(['aide', 'neovim']);
const customPolicy = oscapCustomizationsPolicy('custom-policy-123');
expect(customPolicy.packages).toEqual(['neovim']);
await renderCreateMode();
await selectRhel9();
await selectGuestImageTarget();
await goToOscapStep();
await selectProfile();
await waitFor(() => {
expect(screen.getByText(/aide, neovim/i)).toBeInTheDocument();
});
expect(customPolicy.packages).not.toContain('aide');
expect(customPolicy.packages).toContain('neovim');
expect(normalProfile.packages).toContain('aide');
expect(normalProfile.packages).toContain('neovim');
});
});

View file

@ -513,123 +513,6 @@ describe('Step Packages', () => {
expect(secondAppStreamRow).toBeDisabled();
expect(secondAppStreamRow).not.toBeChecked();
});
test('module selection sorts selected stream to top while maintaining alphabetical order', async () => {
const user = userEvent.setup();
await renderCreateMode();
await goToPackagesStep();
await typeIntoSearchBox('sortingTest');
await screen.findAllByText('alphaModule');
await screen.findAllByText('betaModule');
await screen.findAllByText('gammaModule');
let rows = await screen.findAllByRole('row');
rows.shift();
expect(rows).toHaveLength(6);
expect(rows[0]).toHaveTextContent('alphaModule');
expect(rows[0]).toHaveTextContent('3.0');
expect(rows[1]).toHaveTextContent('alphaModule');
expect(rows[1]).toHaveTextContent('2.0');
expect(rows[2]).toHaveTextContent('betaModule');
expect(rows[2]).toHaveTextContent('4.0');
expect(rows[3]).toHaveTextContent('betaModule');
expect(rows[3]).toHaveTextContent('2.0');
// Select betaModule with stream 2.0 (row index 3)
const betaModule20Checkbox = await screen.findByRole('checkbox', {
name: /select row 3/i,
});
await waitFor(() => user.click(betaModule20Checkbox));
expect(betaModule20Checkbox).toBeChecked();
// After selection, the active stream (2.0) should be prioritized
// All modules with stream 2.0 should move to the top, maintaining alphabetical order
rows = await screen.findAllByRole('row');
rows.shift();
expect(rows[0]).toHaveTextContent('alphaModule');
expect(rows[0]).toHaveTextContent('2.0');
expect(rows[1]).toHaveTextContent('betaModule');
expect(rows[1]).toHaveTextContent('2.0');
expect(rows[2]).toHaveTextContent('gammaModule');
expect(rows[2]).toHaveTextContent('2.0');
expect(rows[3]).toHaveTextContent('alphaModule');
expect(rows[3]).toHaveTextContent('3.0');
expect(rows[4]).toHaveTextContent('betaModule');
expect(rows[4]).toHaveTextContent('4.0');
expect(rows[5]).toHaveTextContent('gammaModule');
expect(rows[5]).toHaveTextContent('1.5');
// Verify that only the selected module is checked
const updatedBetaModule20Checkbox = await screen.findByRole('checkbox', {
name: /select row 1/i, // betaModule 2.0 is now at position 1
});
expect(updatedBetaModule20Checkbox).toBeChecked();
// Verify that only one checkbox is checked
const allCheckboxes = await screen.findAllByRole('checkbox', {
name: /select row [0-9]/i,
});
const checkedCheckboxes = allCheckboxes.filter(
(cb) => (cb as HTMLInputElement).checked,
);
expect(checkedCheckboxes).toHaveLength(1);
expect(checkedCheckboxes[0]).toBe(updatedBetaModule20Checkbox);
});
test('unselecting a module does not cause jumping but may reset sort to default', async () => {
const user = userEvent.setup();
await renderCreateMode();
await goToPackagesStep();
await selectCustomRepo();
await typeIntoSearchBox('sortingTest');
await screen.findAllByText('betaModule');
const betaModule20Checkbox = await screen.findByRole('checkbox', {
name: /select row 3/i,
});
await waitFor(() => user.click(betaModule20Checkbox));
expect(betaModule20Checkbox).toBeChecked();
let rows = await screen.findAllByRole('row');
rows.shift();
expect(rows[0]).toHaveTextContent('alphaModule');
expect(rows[0]).toHaveTextContent('2.0');
expect(rows[1]).toHaveTextContent('betaModule');
expect(rows[1]).toHaveTextContent('2.0');
const updatedBetaModule20Checkbox = await screen.findByRole('checkbox', {
name: /select row 1/i,
});
await waitFor(() => user.click(updatedBetaModule20Checkbox));
expect(updatedBetaModule20Checkbox).not.toBeChecked();
// After unselection, the sort may reset to default or stay the same
// The important thing is that we don't get jumping/reordering during the interaction
rows = await screen.findAllByRole('row');
rows.shift(); // Remove header row
const allCheckboxes = await screen.findAllByRole('checkbox', {
name: /select row [0-9]/i,
});
const checkedCheckboxes = allCheckboxes.filter(
(cb) => (cb as HTMLInputElement).checked,
);
expect(checkedCheckboxes).toHaveLength(0);
// The key test: the table should have a consistent, predictable order
// Either the original alphabetical order OR the stream-sorted order
// What we don't want is jumping around during the selection/unselection process
expect(rows).toHaveLength(6); // Still have all 6 modules
const moduleNames = rows.map((row) => {
const match = row.textContent?.match(/(\w+Module)/);
return match ? match[1] : '';
});
expect(moduleNames).toContain('alphaModule');
expect(moduleNames).toContain('betaModule');
expect(moduleNames).toContain('gammaModule');
});
});
});

View file

@ -109,12 +109,12 @@ describe('Step Services', () => {
router = undefined;
});
test('clicking Next loads Ansible Automation Platform', async () => {
test('clicking Next loads First boot script', async () => {
await renderCreateMode();
await goToServicesStep();
await clickNext();
await screen.findByRole('heading', {
name: 'Ansible Automation Platform',
name: 'First boot configuration',
});
});

View file

@ -36,20 +36,8 @@ export const mockPolicies = {
profile_title: 'DISA STIG with GUI for Red Hat Enterprise Linux 8',
ref_id: 'xccdf_org.ssgproject.content_profile_stig_gui',
},
{
id: 'custom-policy-123',
title: 'Custom CIS Policy (Partial Rules)',
description: 'A customized policy where user removed some rules',
compliance_threshold: 100,
total_system_count: 5,
type: 'policy',
os_major_version: 8,
profile_title:
'Custom CIS Red Hat Enterprise Linux 8 Benchmark for Level 1 - Workstation',
ref_id: 'xccdf_org.ssgproject.content_profile_cis_workstation_l1',
},
],
meta: {
total: 4,
total: 3,
},
};

View file

@ -124,17 +124,9 @@ export const oscapCustomizationsPolicy = (
): GetOscapCustomizationsApiResponse => {
const policyData = mockPolicies.data.find((p) => p.id === policy);
const customizations = oscapCustomizations(policyData!.ref_id);
// Simulate different levels of customization based on policy
if (policy === 'custom-policy-123') {
// This policy has user-customized rules - only neovim remains
customizations.packages = ['neovim']; // User removed aide package
} else {
// Other policies: filter out a single package to simulate basic customizations
customizations.packages = customizations.packages!.filter(
(p) => p !== 'aide',
);
}
// filter out a single package to simulate the customizations being tailored
customizations.packages = customizations.packages!.filter(
(p) => p !== 'aide',
);
return customizations;
};

View file

@ -75,64 +75,6 @@ export const mockSourcesPackagesResults = (
},
];
}
if (search === 'sortingTest') {
return [
{
package_name: 'alphaModule',
summary: 'Alpha module for sorting tests',
package_sources: [
{
name: 'alphaModule',
type: 'module',
stream: '2.0',
end_date: '2025-12-01',
},
{
name: 'alphaModule',
type: 'module',
stream: '3.0',
end_date: '2027-12-01',
},
],
},
{
package_name: 'betaModule',
summary: 'Beta module for sorting tests',
package_sources: [
{
name: 'betaModule',
type: 'module',
stream: '2.0',
end_date: '2025-06-01',
},
{
name: 'betaModule',
type: 'module',
stream: '4.0',
end_date: '2028-06-01',
},
],
},
{
package_name: 'gammaModule',
summary: 'Gamma module for sorting tests',
package_sources: [
{
name: 'gammaModule',
type: 'module',
stream: '2.0',
end_date: '2025-08-01',
},
{
name: 'gammaModule',
type: 'module',
stream: '1.5',
end_date: '2026-08-01',
},
],
},
];
}
if (search === 'mock') {
return [
{

View file

@ -64,8 +64,6 @@ vi.mock('@unleash/proxy-client-react', () => ({
return true;
case 'image-builder.templates.enabled':
return true;
case 'image-builder.aap.enabled':
return true;
default:
return false;
}