Compare commits
31 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
080513ad6d | ||
|
|
7391652e17 | ||
|
|
9a17373234 | ||
|
|
d7f844b8b6 | ||
|
|
859b7cace8 | ||
|
|
3a83a14720 | ||
|
|
e61cb99f1b | ||
|
|
a5aa15cbcb | ||
|
|
44c3674072 | ||
|
|
4d783537fb | ||
|
|
0b96c64c93 | ||
|
|
0d917c3cd8 | ||
|
|
957700adcc | ||
|
|
fa0560ac4d | ||
|
|
0e7f5d9e7b | ||
|
|
e0dd33fdc9 | ||
|
|
b0393a5f4f | ||
|
|
a9d2ba59a8 | ||
|
|
af19251f17 | ||
|
|
090544c333 | ||
|
|
4b188a0393 | ||
|
|
63f55c7408 | ||
|
|
bc3288a83e | ||
|
|
3e5c5dca76 | ||
|
|
04f0528701 | ||
|
|
3e2e9dcaa6 | ||
|
|
54e413f459 | ||
|
|
f6f6e58449 | ||
|
|
bf77501eea | ||
|
|
42b16bafd8 | ||
|
|
04adcc133c |
57 changed files with 2743 additions and 1801 deletions
257
.forgejo/workflows/ci.yml
Normal file
257
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
---
|
||||
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
|
||||
257
.forgejo/workflows/ci.yml.disabled
Normal file
257
.forgejo/workflows/ci.yml.disabled
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
---
|
||||
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
|
||||
|
|
@ -32,8 +32,7 @@ test:
|
|||
- RUNNER:
|
||||
- aws/fedora-41-x86_64
|
||||
- aws/fedora-42-x86_64
|
||||
- aws/rhel-9.6-nightly-x86_64
|
||||
- aws/rhel-10.0-nightly-x86_64
|
||||
- aws/rhel-10.1-nightly-x86_64
|
||||
INTERNAL_NETWORK: ["true"]
|
||||
|
||||
finish:
|
||||
|
|
|
|||
|
|
@ -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:76bc23ca84c6b31251fee267f417ed262d9ea49655e942d1978149e137295bb0
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
||||
- 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:09ca1ce263bb686b08187a3b836f6a5bd54875e8596105e4d7d816d57caf55a0
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
||||
- 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:4bafcaab0f0c998a89a1cc33bdbbf74f39eea52e6c0e43013c356a322f94940f
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
||||
- 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:5446102f233991bdc73451201adf361766d45c638f3d89f19121ae1c2ba8bf17
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
|
|||
|
|
@ -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:76bc23ca84c6b31251fee267f417ed262d9ea49655e942d1978149e137295bb0
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
||||
- 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:09ca1ce263bb686b08187a3b836f6a5bd54875e8596105e4d7d816d57caf55a0
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
||||
- 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:4bafcaab0f0c998a89a1cc33bdbbf74f39eea52e6c0e43013c356a322f94940f
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
||||
- 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:5446102f233991bdc73451201adf361766d45c638f3d89f19121ae1c2ba8bf17
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
Name: cockpit-image-builder
|
||||
Version: 74
|
||||
Version: 76
|
||||
Release: 1%{?dist}
|
||||
Summary: Image builder plugin for Cockpit
|
||||
|
||||
|
|
|
|||
1848
package-lock.json
generated
1848
package-lock.json
generated
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
|
@ -8,16 +8,17 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ltd/j-toml": "1.38.0",
|
||||
"@patternfly/patternfly": "6.3.0",
|
||||
"@patternfly/patternfly": "6.3.1",
|
||||
"@patternfly/react-code-editor": "6.3.1",
|
||||
"@patternfly/react-core": "6.3.0",
|
||||
"@patternfly/react-core": "6.3.1",
|
||||
"@patternfly/react-table": "6.3.1",
|
||||
"@redhat-cloud-services/frontend-components": "7.0.3",
|
||||
"@redhat-cloud-services/frontend-components-notifications": "6.1.3",
|
||||
"@redhat-cloud-services/frontend-components-notifications": "6.1.5",
|
||||
"@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.0",
|
||||
"@sentry/webpack-plugin": "4.1.1",
|
||||
"@unleash/proxy-client-react": "5.0.1",
|
||||
"classnames": "2.5.1",
|
||||
"jwt-decode": "4.0.0",
|
||||
|
|
@ -46,13 +47,13 @@
|
|||
"@testing-library/jest-dom": "6.6.4",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/node": "24.1.0",
|
||||
"@types/node": "24.3.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.39.0",
|
||||
"@typescript-eslint/parser": "8.39.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"babel-loader": "10.0.0",
|
||||
|
|
@ -61,13 +62,13 @@
|
|||
"chartjs-plugin-annotation": "3.1.0",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"css-loader": "7.1.2",
|
||||
"eslint": "9.32.0",
|
||||
"eslint": "9.33.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.3",
|
||||
"eslint-plugin-prettier": "5.5.4",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-redux": "4.2.2",
|
||||
|
|
@ -80,7 +81,7 @@
|
|||
"madge": "8.0.0",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"moment": "2.30.1",
|
||||
"msw": "2.10.4",
|
||||
"msw": "2.10.5",
|
||||
"npm-run-all": "4.1.5",
|
||||
"path-browserify": "1.0.1",
|
||||
"postcss-scss": "4.0.9",
|
||||
|
|
@ -88,12 +89,12 @@
|
|||
"redux-mock-store": "1.5.5",
|
||||
"sass": "1.90.0",
|
||||
"sass-loader": "16.0.5",
|
||||
"stylelint": "16.23.0",
|
||||
"stylelint-config-recommended-scss": "15.0.1",
|
||||
"stylelint": "16.23.1",
|
||||
"stylelint-config-recommended-scss": "16.0.0",
|
||||
"ts-node": "10.9.2",
|
||||
"ts-patch": "3.3.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.38.0",
|
||||
"typescript-eslint": "8.40.0",
|
||||
"uuid": "11.1.0",
|
||||
"vitest": "3.2.4",
|
||||
"vitest-canvas-mock": "0.3.3",
|
||||
|
|
|
|||
|
|
@ -22,10 +22,8 @@ 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
|
||||
|
|
|
|||
214
playwright/Customizations/AAP.spec.ts
Normal file
214
playwright/Customizations/AAP.spec.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -67,8 +67,10 @@ 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']}`,
|
||||
`${(osRel as any)['ID']}-${(osRel as any)['VERSION_ID'].split('.')[0]}`,
|
||||
);
|
||||
|
||||
if (distro === undefined) {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,11 @@ 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();
|
||||
|
|
@ -87,7 +92,12 @@ test.describe.serial('test', () => {
|
|||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
||||
|
||||
await expect(
|
||||
frame.locator('.pf-v6-c-card__title-text').getByText(blueprintName),
|
||||
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,
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
7b4735d287dd0950e0a6f47dde65b62b0f239da1
|
||||
cf0a810fd3b75fa27139746c4dfe72222e13dcba
|
||||
|
|
|
|||
|
|
@ -50,11 +50,21 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
|
|||
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
||||
}}
|
||||
>
|
||||
<CardTitle>
|
||||
<CardTitle aria-label={blueprint.name}>
|
||||
{isLoading && blueprint.id === selectedBlueprintId && (
|
||||
<Spinner size='md' />
|
||||
)}
|
||||
{blueprint.name}
|
||||
{
|
||||
// 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
|
||||
}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>{blueprint.description}</CardBody>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
Bullseye,
|
||||
|
|
@ -17,7 +17,6 @@ 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';
|
||||
|
||||
|
|
@ -29,6 +28,7 @@ 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,16 +73,6 @@ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -16,11 +16,13 @@ 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 } from '../../Hooks';
|
||||
import {
|
||||
useComposeBPWithNotification as useComposeBlueprintMutation,
|
||||
useGetUser,
|
||||
} from '../../Hooks';
|
||||
import { useGetBlueprintQuery } from '../../store/backendApi';
|
||||
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
|
||||
import { useAppSelector } from '../../store/hooks';
|
||||
|
|
@ -37,18 +39,7 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
|||
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
||||
useComposeBlueprintMutation();
|
||||
const { analytics, auth } = useChrome();
|
||||
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
const onBuildHandler = async () => {
|
||||
if (selectedBlueprintId) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -9,14 +9,16 @@ 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 } from '../../Hooks';
|
||||
import {
|
||||
useDeleteBPWithNotification as useDeleteBlueprintMutation,
|
||||
useGetUser,
|
||||
} from '../../Hooks';
|
||||
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
||||
import {
|
||||
selectBlueprintSearchInput,
|
||||
|
|
@ -42,17 +44,7 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
|||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||
const dispatch = useAppDispatch();
|
||||
const { analytics, auth } = useChrome();
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
const searchParams: GetBlueprintsApiArg = {
|
||||
limit: blueprintsLimit,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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';
|
||||
|
|
@ -40,6 +41,7 @@ import UsersStep from './steps/Users';
|
|||
import { getHostArch, getHostDistro } from './utilities/getHostInfo';
|
||||
import { useHasSpecificTargetOnly } from './utilities/hasSpecificTargetOnly';
|
||||
import {
|
||||
useAAPValidation,
|
||||
useDetailsValidation,
|
||||
useFilesystemValidation,
|
||||
useFirewallValidation,
|
||||
|
|
@ -197,6 +199,7 @@ 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(() => {
|
||||
|
|
@ -283,6 +286,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
const firewallValidation = useFirewallValidation();
|
||||
// Services
|
||||
const servicesValidation = useServicesValidation();
|
||||
// AAP
|
||||
const aapValidation = useAAPValidation();
|
||||
// Firstboot
|
||||
const firstBootValidation = useFirstBootValidation();
|
||||
// Details
|
||||
|
|
@ -293,8 +298,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
const hasWslTargetOnly = useHasSpecificTargetOnly('wsl');
|
||||
|
||||
let startIndex = 1; // default index
|
||||
const JUMP_TO_REVIEW_STEP = 23;
|
||||
|
||||
if (isEdit) {
|
||||
startIndex = 22;
|
||||
startIndex = JUMP_TO_REVIEW_STEP;
|
||||
}
|
||||
|
||||
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
|
||||
|
|
@ -655,6 +662,22 @@ 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'
|
||||
|
|
|
|||
|
|
@ -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,6 +138,7 @@ export const ValidatedInput = ({
|
|||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
...props
|
||||
}: ValidatedTextInputPropTypes) => {
|
||||
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
||||
|
||||
|
|
@ -164,6 +165,7 @@ export const ValidatedInput = ({
|
|||
aria-label={ariaLabel || ''}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder || ''}
|
||||
{...props}
|
||||
/>
|
||||
{!isPristine && !validator(value) && (
|
||||
<HelperText>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
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;
|
||||
18
src/Components/CreateImageWizard/steps/AAP/index.tsx
Normal file
18
src/Components/CreateImageWizard/steps/AAP/index.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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;
|
||||
|
|
@ -8,7 +8,10 @@ import {
|
|||
Spinner,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { useGetOscapCustomizationsQuery } from '../../../../../store/backendApi';
|
||||
import {
|
||||
useGetComplianceCustomizationsQuery,
|
||||
useGetOscapCustomizationsQuery,
|
||||
} from '../../../../../store/backendApi';
|
||||
import { PolicyRead, usePolicyQuery } from '../../../../../store/complianceApi';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
||||
import { OpenScapProfile } from '../../../../../store/imageBuilderApi';
|
||||
|
|
@ -16,6 +19,7 @@ import {
|
|||
changeCompliance,
|
||||
selectCompliancePolicyID,
|
||||
selectComplianceProfileID,
|
||||
selectComplianceType,
|
||||
selectDistribution,
|
||||
selectFips,
|
||||
} from '../../../../../store/wizardSlice';
|
||||
|
|
@ -31,12 +35,29 @@ 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,
|
||||
|
|
@ -48,6 +69,20 @@ 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,
|
||||
|
|
@ -74,23 +109,28 @@ export const OscapProfileInformation = ({
|
|||
policyTitle: pol.title,
|
||||
}),
|
||||
);
|
||||
}, [isSuccessPolicyInfo]);
|
||||
}, [isSuccessPolicyInfo, dispatch, policyInfo]);
|
||||
|
||||
const oscapProfile = oscapProfileInfo?.openscap as OpenScapProfile;
|
||||
const oscapProfile = profileMetadata?.openscap as OpenScapProfile | undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isFetchingOscapProfileInfo || isFetchingPolicyInfo) && (
|
||||
<Spinner size='lg' />
|
||||
{(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>
|
||||
)}
|
||||
{isSuccessOscapProfileInfo && (
|
||||
{shouldShowData && (
|
||||
<>
|
||||
<Content component={ContentVariants.dl} className='review-step-dl'>
|
||||
<Content
|
||||
component={ContentVariants.dt}
|
||||
className='pf-v6-u-min-width'
|
||||
>
|
||||
Profile description
|
||||
{complianceType === 'compliance'
|
||||
? 'Policy description'
|
||||
: 'Profile description'}
|
||||
</Content>
|
||||
<Content component={ContentVariants.dd}>
|
||||
{oscapProfile?.profile_description}
|
||||
|
|
@ -116,7 +156,7 @@ export const OscapProfileInformation = ({
|
|||
<Content component={ContentVariants.dd}>
|
||||
<CodeBlock>
|
||||
<CodeBlockCode>
|
||||
{(oscapProfileInfo?.packages ?? []).join(', ')}
|
||||
{(customizationData?.packages ?? []).join(', ')}
|
||||
</CodeBlockCode>
|
||||
</CodeBlock>
|
||||
</Content>
|
||||
|
|
@ -129,7 +169,7 @@ export const OscapProfileInformation = ({
|
|||
<Content component={ContentVariants.dd}>
|
||||
<CodeBlock>
|
||||
<CodeBlockCode>
|
||||
{oscapProfileInfo?.kernel?.append}
|
||||
{customizationData?.kernel?.append}
|
||||
</CodeBlockCode>
|
||||
</CodeBlock>
|
||||
</Content>
|
||||
|
|
@ -142,7 +182,7 @@ export const OscapProfileInformation = ({
|
|||
<Content component={ContentVariants.dd}>
|
||||
<CodeBlock>
|
||||
<CodeBlockCode>
|
||||
{(oscapProfileInfo?.services?.enabled ?? []).join(' ')}
|
||||
{(customizationData?.services?.enabled ?? []).join(' ')}
|
||||
</CodeBlockCode>
|
||||
</CodeBlock>
|
||||
</Content>
|
||||
|
|
@ -155,8 +195,8 @@ export const OscapProfileInformation = ({
|
|||
<Content component={ContentVariants.dd}>
|
||||
<CodeBlock>
|
||||
<CodeBlockCode>
|
||||
{(oscapProfileInfo?.services?.disabled ?? [])
|
||||
.concat(oscapProfileInfo?.services?.masked ?? [])
|
||||
{(customizationData?.services?.disabled ?? [])
|
||||
.concat(customizationData?.services?.masked ?? [])
|
||||
.join(' ')}
|
||||
</CodeBlockCode>
|
||||
</CodeBlock>
|
||||
|
|
|
|||
|
|
@ -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 } = useGetOscapCustomizationsForPolicyQuery(
|
||||
const { data: currentProfileData } = useGetComplianceCustomizationsQuery(
|
||||
{
|
||||
distribution: release,
|
||||
policy: policyID!,
|
||||
|
|
@ -105,7 +105,7 @@ const PolicySelector = () => {
|
|||
{ skip: !policyID },
|
||||
);
|
||||
|
||||
const [trigger] = useLazyGetOscapCustomizationsForPolicyQuery();
|
||||
const [trigger] = useLazyGetComplianceCustomizationsQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (!policies || policies.data === undefined) {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import {
|
|||
Thead,
|
||||
Tr,
|
||||
} from '@patternfly/react-table';
|
||||
import { orderBy } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import CustomHelperText from './components/CustomHelperText';
|
||||
|
|
@ -66,7 +67,6 @@ import {
|
|||
} from '../../../../constants';
|
||||
import { useGetArchitecturesQuery } from '../../../../store/backendApi';
|
||||
import {
|
||||
ApiPackageSourcesResponse,
|
||||
ApiRepositoryResponseRead,
|
||||
ApiSearchRpmResponse,
|
||||
useCreateRepositoryMutation,
|
||||
|
|
@ -700,7 +700,7 @@ const Packages = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const unpackedData: IBPackageWithRepositoryInfo[] =
|
||||
let unpackedData: IBPackageWithRepositoryInfo[] =
|
||||
combinedPackageData.flatMap((item) => {
|
||||
// Spread modules into separate rows by application stream
|
||||
if (item.sources) {
|
||||
|
|
@ -724,13 +724,16 @@ const Packages = () => {
|
|||
});
|
||||
|
||||
// group by name, but sort by application stream in descending order
|
||||
unpackedData.sort((a, b) => {
|
||||
if (a.name === b.name) {
|
||||
return (b.stream ?? '').localeCompare(a.stream ?? '');
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
});
|
||||
unpackedData = orderBy(
|
||||
unpackedData,
|
||||
[
|
||||
'name',
|
||||
(pkg) => pkg.stream || '',
|
||||
(pkg) => pkg.repository || '',
|
||||
(pkg) => pkg.module_name || '',
|
||||
],
|
||||
['asc', 'desc', 'asc', 'asc'],
|
||||
);
|
||||
|
||||
if (toggleSelected === 'toggle-available') {
|
||||
if (activeTabKey === Repos.INCLUDED) {
|
||||
|
|
@ -866,8 +869,6 @@ const Packages = () => {
|
|||
dispatch(addPackage(pkg));
|
||||
if (pkg.type === 'module') {
|
||||
setActiveStream(pkg.stream || '');
|
||||
setActiveSortIndex(2);
|
||||
setPage(1);
|
||||
dispatch(
|
||||
addModule({
|
||||
name: pkg.module_name || '',
|
||||
|
|
@ -993,7 +994,18 @@ const Packages = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const initialExpandedPkgs: IBPackageWithRepositoryInfo[] = [];
|
||||
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 [expandedPkgs, setExpandedPkgs] = useState(initialExpandedPkgs);
|
||||
|
||||
const setPkgExpanded = (
|
||||
|
|
@ -1001,12 +1013,13 @@ const Packages = () => {
|
|||
isExpanding: boolean,
|
||||
) =>
|
||||
setExpandedPkgs((prevExpanded) => {
|
||||
const otherExpandedPkgs = prevExpanded.filter((p) => p.name !== pkg.name);
|
||||
return isExpanding ? [...otherExpandedPkgs, pkg] : otherExpandedPkgs;
|
||||
const pkgKey = getPackageUniqueKey(pkg);
|
||||
const otherExpandedPkgs = prevExpanded.filter((key) => key !== pkgKey);
|
||||
return isExpanding ? [...otherExpandedPkgs, pkgKey] : otherExpandedPkgs;
|
||||
});
|
||||
|
||||
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
||||
expandedPkgs.includes(pkg);
|
||||
expandedPkgs.includes(getPackageUniqueKey(pkg));
|
||||
|
||||
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
||||
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
||||
|
|
@ -1030,51 +1043,37 @@ const Packages = () => {
|
|||
'asc' | 'desc'
|
||||
>('asc');
|
||||
|
||||
const getSortableRowValues = (
|
||||
pkg: IBPackageWithRepositoryInfo,
|
||||
): (string | number | ApiPackageSourcesResponse[] | undefined)[] => {
|
||||
return [pkg.name, pkg.summary, pkg.stream, pkg.end_date, pkg.repository];
|
||||
};
|
||||
const sortedPackages = useMemo(() => {
|
||||
if (!transformedPackages || !Array.isArray(transformedPackages)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
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]);
|
||||
|
||||
const getSortParams = (columnIndex: number) => ({
|
||||
sortBy: {
|
||||
|
|
@ -1100,14 +1099,14 @@ const Packages = () => {
|
|||
(module) => module.name === pkg.name,
|
||||
);
|
||||
isSelected =
|
||||
packages.some((p) => p.name === pkg.name) && !isModuleWithSameName;
|
||||
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
||||
!isModuleWithSameName;
|
||||
}
|
||||
|
||||
if (pkg.type === 'module') {
|
||||
// the package is selected if it's added to the packages state
|
||||
// and its module stream matches one in enabled_modules
|
||||
// the package is selected if its module stream matches one in enabled_modules
|
||||
isSelected =
|
||||
packages.some((p) => p.name === pkg.name) &&
|
||||
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
||||
modules.some(
|
||||
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
|
||||
);
|
||||
|
|
@ -1208,7 +1207,7 @@ const Packages = () => {
|
|||
.slice(computeStart(), computeEnd())
|
||||
.map((grp, rowIndex) => (
|
||||
<Tbody
|
||||
key={`${grp.name}-${rowIndex}`}
|
||||
key={`${grp.name}-${grp.repository || 'default'}`}
|
||||
isExpanded={isGroupExpanded(grp.name)}
|
||||
>
|
||||
<Tr data-testid='package-row'>
|
||||
|
|
@ -1308,7 +1307,7 @@ const Packages = () => {
|
|||
.slice(computeStart(), computeEnd())
|
||||
.map((pkg, rowIndex) => (
|
||||
<Tbody
|
||||
key={`${pkg.name}-${rowIndex}`}
|
||||
key={`${pkg.name}-${pkg.stream || 'default'}-${pkg.module_name || pkg.name}`}
|
||||
isExpanded={isPkgExpanded(pkg)}
|
||||
>
|
||||
<Tr data-testid='package-row'>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
ClipboardCopy,
|
||||
|
|
@ -16,6 +16,7 @@ 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,
|
||||
|
|
@ -24,18 +25,7 @@ import {
|
|||
|
||||
const RegistrationStep = () => {
|
||||
const { auth } = useChrome();
|
||||
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 { orgId } = useGetUser(auth);
|
||||
|
||||
const activationKey = useAppSelector(selectActivationKey);
|
||||
const registrationType = useAppSelector(selectRegistrationType);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { 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,19 +44,8 @@ export const CreateSaveAndBuildBtn = ({
|
|||
setIsOpen,
|
||||
isDisabled,
|
||||
}: CreateDropdownProps) => {
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
|
||||
const { analytics, auth, isBeta } = useChrome();
|
||||
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
const packages = useAppSelector(selectPackages);
|
||||
|
||||
|
|
@ -113,17 +102,7 @@ export const CreateSaveButton = ({
|
|||
isDisabled,
|
||||
}: CreateDropdownProps) => {
|
||||
const { analytics, auth, isBeta } = useChrome();
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
const packages = useAppSelector(selectPackages);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React 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,19 +37,8 @@ export const EditSaveAndBuildBtn = ({
|
|||
blueprintId,
|
||||
isDisabled,
|
||||
}: EditDropdownProps) => {
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
|
||||
const { analytics, auth, isBeta } = useChrome();
|
||||
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
const { trigger: buildBlueprint } = useComposeBlueprintMutation();
|
||||
const packages = useAppSelector(selectPackages);
|
||||
|
|
@ -105,19 +94,8 @@ export const EditSaveButton = ({
|
|||
blueprintId,
|
||||
isDisabled,
|
||||
}: EditDropdownProps) => {
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
|
||||
const { analytics, auth, isBeta } = useChrome();
|
||||
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
const packages = useAppSelector(selectPackages);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { EditSaveAndBuildBtn, EditSaveButton } from './EditDropdown';
|
|||
|
||||
import {
|
||||
useCreateBPWithNotification as useCreateBlueprintMutation,
|
||||
useGetUser,
|
||||
useUpdateBPWithNotification as useUpdateBlueprintMutation,
|
||||
} from '../../../../../Hooks';
|
||||
import { resolveRelPath } from '../../../../../Utilities/path';
|
||||
|
|
@ -33,6 +34,7 @@ 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();
|
||||
|
|
@ -52,14 +54,12 @@ 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 should be fine on-prem, we should
|
||||
// be able to ignore the `org-id`
|
||||
// NOTE: This is fine for on prem because we save the org id
|
||||
// to state through a form field in the registration step
|
||||
return mapRequestFromState(store, '');
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
KernelList,
|
||||
LocaleList,
|
||||
OscapList,
|
||||
RegisterAapList,
|
||||
RegisterLaterList,
|
||||
RegisterNowList,
|
||||
RegisterSatelliteList,
|
||||
|
|
@ -42,6 +43,7 @@ import isRhel from '../../../../../src/Utilities/isRhel';
|
|||
import { targetOptions } from '../../../../constants';
|
||||
import { useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
selectAapRegistration,
|
||||
selectBlueprintDescription,
|
||||
selectBlueprintName,
|
||||
selectCompliancePolicyID,
|
||||
|
|
@ -65,6 +67,7 @@ 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);
|
||||
|
|
@ -83,6 +86,7 @@ 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);
|
||||
|
|
@ -101,6 +105,8 @@ 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) =>
|
||||
|
|
@ -499,6 +505,21 @@ 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(
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ import { useAppSelector } from '../../../../store/hooks';
|
|||
import { useGetSourceListQuery } from '../../../../store/provisioningApi';
|
||||
import { useShowActivationKeyQuery } from '../../../../store/rhsmApi';
|
||||
import {
|
||||
selectAapCallbackUrl,
|
||||
selectAapHostConfigKey,
|
||||
selectAapTlsCertificateAuthority,
|
||||
selectAapTlsConfirmation,
|
||||
selectActivationKey,
|
||||
selectArchitecture,
|
||||
selectAwsAccountId,
|
||||
|
|
@ -660,6 +664,45 @@ 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);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
CockpitUploadTypes,
|
||||
} from '../../../store/cockpit/types';
|
||||
import {
|
||||
AapRegistration,
|
||||
AwsUploadRequestOptions,
|
||||
AzureUploadRequestOptions,
|
||||
BlueprintExportResponse,
|
||||
|
|
@ -49,6 +50,11 @@ import { ApiRepositoryImportResponseRead } from '../../../store/service/contentS
|
|||
import {
|
||||
ComplianceType,
|
||||
initialState,
|
||||
RegistrationType,
|
||||
selectAapCallbackUrl,
|
||||
selectAapHostConfigKey,
|
||||
selectAapTlsCertificateAuthority,
|
||||
selectAapTlsConfirmation,
|
||||
selectActivationKey,
|
||||
selectArchitecture,
|
||||
selectAwsAccountId,
|
||||
|
|
@ -205,8 +211,9 @@ function commonRequestToState(
|
|||
snapshot_date = '';
|
||||
}
|
||||
|
||||
// we need to check for the region for on-prem
|
||||
const awsUploadOptions = aws?.upload_request
|
||||
.options as AwsUploadRequestOptions;
|
||||
.options as AwsUploadRequestOptions & { region?: string | undefined };
|
||||
const gcpUploadOptions = gcp?.upload_request
|
||||
.options as GcpUploadRequestOptions;
|
||||
const azureUploadOptions = azure?.upload_request
|
||||
|
|
@ -309,6 +316,7 @@ 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,
|
||||
|
|
@ -387,14 +395,7 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
|||
baseUrl: request.customizations.subscription?.['base-url'] || '',
|
||||
},
|
||||
registration: {
|
||||
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',
|
||||
registrationType: getRegistrationType(request),
|
||||
activationKey: isRhel(request.distribution)
|
||||
? request.customizations.subscription?.['activation-key']
|
||||
: undefined,
|
||||
|
|
@ -403,6 +404,15 @@ 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),
|
||||
};
|
||||
};
|
||||
|
|
@ -452,6 +462,15 @@ 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),
|
||||
};
|
||||
};
|
||||
|
|
@ -461,6 +480,24 @@ 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[] => {
|
||||
|
|
@ -482,6 +519,24 @@ 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,
|
||||
|
|
@ -642,6 +697,7 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
|
|||
pem_certs: [satCert],
|
||||
}
|
||||
: undefined,
|
||||
aap_registration: getAapRegistration(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import { useAppSelector } from '../../../store/hooks';
|
|||
import { BlueprintsResponse } from '../../../store/imageBuilderApi';
|
||||
import { useShowActivationKeyQuery } from '../../../store/rhsmApi';
|
||||
import {
|
||||
selectAapCallbackUrl,
|
||||
selectAapHostConfigKey,
|
||||
selectAapTlsCertificateAuthority,
|
||||
selectAapTlsConfirmation,
|
||||
selectActivationKey,
|
||||
selectBlueprintDescription,
|
||||
selectBlueprintId,
|
||||
|
|
@ -54,6 +58,8 @@ import {
|
|||
isSshKeyValid,
|
||||
isUserGroupValid,
|
||||
isUserNameValid,
|
||||
isValidUrl,
|
||||
validateMultipleCertificates,
|
||||
} from '../validators';
|
||||
|
||||
export type StepValidation = {
|
||||
|
|
@ -205,6 +211,62 @@ 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);
|
||||
|
|
|
|||
|
|
@ -138,3 +138,85 @@ 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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React 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 />;
|
||||
};
|
||||
|
||||
const parseGcpSharedWith = (
|
||||
export const parseGcpSharedWith = (
|
||||
sharedWith: GcpUploadRequestOptions['share_with_accounts'],
|
||||
) => {
|
||||
if (sharedWith) {
|
||||
|
|
@ -134,19 +134,9 @@ 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();
|
||||
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
if (!isAwsUploadRequestOptions(options)) {
|
||||
throw TypeError(
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
Tr,
|
||||
} from '@patternfly/react-table';
|
||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||
import { useFlag } from '@unleash/proxy-client-react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { NavigateFunction, useNavigate } from 'react-router-dom';
|
||||
|
||||
|
|
@ -58,6 +58,7 @@ import {
|
|||
SEARCH_INPUT,
|
||||
STATUS_POLLING_INTERVAL,
|
||||
} from '../../constants';
|
||||
import { useGetUser } from '../../Hooks';
|
||||
import {
|
||||
useGetBlueprintComposesQuery,
|
||||
useGetBlueprintsQuery,
|
||||
|
|
@ -87,11 +88,12 @@ 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 =
|
||||
|
|
@ -104,16 +106,7 @@ const ImagesTable = () => {
|
|||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||
|
||||
const { analytics, auth } = useChrome();
|
||||
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
const searchParamsGetBlueprints: GetBlueprintsApiArg = {
|
||||
limit: blueprintsLimit,
|
||||
|
|
@ -382,8 +375,14 @@ type AzureRowPropTypes = {
|
|||
};
|
||||
|
||||
const AzureRow = ({ compose, rowIndex }: AzureRowPropTypes) => {
|
||||
const launchEofFlag = useFlag('image-builder.launcheof');
|
||||
|
||||
const details = <AzureDetails compose={compose} />;
|
||||
const instance = <CloudInstance compose={compose} />;
|
||||
const instance = launchEofFlag ? (
|
||||
<AzureLaunchModal compose={compose} />
|
||||
) : (
|
||||
<CloudInstance compose={compose} />
|
||||
);
|
||||
const status = <CloudStatus compose={compose} />;
|
||||
|
||||
return (
|
||||
|
|
@ -403,13 +402,18 @@ 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 = <OciInstance compose={compose} isExpired={isExpired} />;
|
||||
const instance = launchEofFlag ? (
|
||||
<OciLaunchModal compose={compose} isExpired={isExpired} />
|
||||
) : (
|
||||
<OciInstance compose={compose} isExpired={isExpired} />
|
||||
);
|
||||
const status = (
|
||||
<ExpiringStatus
|
||||
compose={compose}
|
||||
|
|
@ -467,18 +471,8 @@ type AwsRowPropTypes = {
|
|||
|
||||
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
||||
const navigate = useNavigate();
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
const { analytics, auth } = useChrome();
|
||||
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
const target = <AwsTarget compose={compose} />;
|
||||
const status = <CloudStatus compose={compose} />;
|
||||
|
|
@ -553,18 +547,8 @@ const Row = ({
|
|||
details,
|
||||
instance,
|
||||
}: RowPropTypes) => {
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
const { analytics, auth } = useChrome();
|
||||
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const handleToggle = () => setIsExpanded(!isExpanded);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Suspense, useEffect, useState } from 'react';
|
||||
import React, { Suspense, useState } from 'react';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
|
|
@ -20,7 +20,6 @@ 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';
|
||||
|
|
@ -31,6 +30,7 @@ import {
|
|||
MODAL_ANCHOR,
|
||||
SEARCH_INPUT,
|
||||
} from '../../constants';
|
||||
import { useGetUser } from '../../Hooks';
|
||||
import {
|
||||
useGetBlueprintsQuery,
|
||||
useGetComposeStatusQuery,
|
||||
|
|
@ -54,7 +54,10 @@ 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;
|
||||
|
|
@ -97,21 +100,12 @@ const ProvisioningLink = ({
|
|||
compose,
|
||||
composeStatus,
|
||||
}: ProvisioningLinkPropTypes) => {
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
const launchEofFlag = useFlag('image-builder.launcheof');
|
||||
|
||||
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 [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [exposedScalprumModule, error] = useLoadModule(
|
||||
{
|
||||
scope: 'provisioning',
|
||||
|
|
@ -182,7 +176,7 @@ const ProvisioningLink = ({
|
|||
account_id: userData?.identity.internal?.account_id || 'Not found',
|
||||
});
|
||||
|
||||
setWizardOpen(true);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Launch
|
||||
|
|
@ -202,6 +196,10 @@ const ProvisioningLink = ({
|
|||
</Popover>
|
||||
);
|
||||
|
||||
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
|
||||
setIsModalOpen(!isModalOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback='loading...'>
|
||||
|
|
@ -209,7 +207,23 @@ const ProvisioningLink = ({
|
|||
compose.blueprint_version !== selectedBlueprintVersion
|
||||
? buttonWithTooltip
|
||||
: btn}
|
||||
{wizardOpen && (
|
||||
{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 && (
|
||||
<Modal
|
||||
isOpen
|
||||
appendTo={appendTo}
|
||||
|
|
@ -218,7 +232,7 @@ const ProvisioningLink = ({
|
|||
>
|
||||
<ProvisioningWizard
|
||||
hasAccess={permissions[provider]}
|
||||
onClose={() => setWizardOpen(false)}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
image={{
|
||||
name: compose.image_name || compose.id,
|
||||
id: compose.id,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React 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,18 +122,8 @@ export const CloudStatus = ({ compose }: CloudStatusPropTypes) => {
|
|||
const { data, isSuccess } = useGetComposeStatusQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
const { analytics, auth } = useChrome();
|
||||
|
||||
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 { userData } = useGetUser(auth);
|
||||
|
||||
if (!isSuccess) {
|
||||
return <Skeleton />;
|
||||
|
|
|
|||
104
src/Components/Launch/AWSLaunchModal.tsx
Normal file
104
src/Components/Launch/AWSLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
116
src/Components/Launch/AzureLaunchModal.tsx
Normal file
116
src/Components/Launch/AzureLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
169
src/Components/Launch/GcpLaunchModal.tsx
Normal file
169
src/Components/Launch/GcpLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
139
src/Components/Launch/OciLaunchModal.tsx
Normal file
139
src/Components/Launch/OciLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
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'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>
|
||||
);
|
||||
};
|
||||
34
src/Components/Launch/useGenerateDefaultName.ts
Normal file
34
src/Components/Launch/useGenerateDefaultName.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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}`;
|
||||
};
|
||||
|
|
@ -156,7 +156,7 @@ const RegionsSelect = ({ composeId, handleClose }: RegionsSelectPropTypes) => {
|
|||
<MenuToggle
|
||||
variant='typeahead'
|
||||
onClick={handleToggle}
|
||||
innerRef={toggleRef}
|
||||
ref={toggleRef}
|
||||
isExpanded={isOpen}
|
||||
>
|
||||
<TextInputGroup isPlain>
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ export { useDeleteBPWithNotification } from './MutationNotifications/useDeleteBP
|
|||
export { useFixupBPWithNotification } from './MutationNotifications/useFixupBPWithNotification';
|
||||
export { useComposeBPWithNotification } from './MutationNotifications/useComposeBPWithNotification';
|
||||
export { useCloneComposeWithNotification } from './MutationNotifications/useCloneComposeWithNotification';
|
||||
export { useGetUser } from './useGetUser';
|
||||
|
|
|
|||
24
src/Hooks/useGetUser.tsx
Normal file
24
src/Hooks/useGetUser.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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 };
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.10.4'
|
||||
const PACKAGE_VERSION = '2.10.5'
|
||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ 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;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ export type RegistrationType =
|
|||
| 'register-now'
|
||||
| 'register-now-insights'
|
||||
| 'register-now-rhc'
|
||||
| 'register-satellite';
|
||||
| 'register-satellite'
|
||||
| 'register-aap';
|
||||
|
||||
export type ComplianceType = 'openscap' | 'compliance';
|
||||
|
||||
|
|
@ -89,6 +90,12 @@ 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;
|
||||
|
|
@ -189,6 +196,12 @@ export const initialState: wizardState = {
|
|||
architecture: X86_64,
|
||||
distribution: RHEL_10,
|
||||
imageTypes: [],
|
||||
aapRegistration: {
|
||||
callbackUrl: undefined,
|
||||
hostConfigKey: undefined,
|
||||
tlsCertificateAuthority: undefined,
|
||||
skipTlsVerification: undefined,
|
||||
},
|
||||
aws: {
|
||||
accountId: '',
|
||||
shareMethod: 'sources',
|
||||
|
|
@ -376,6 +389,26 @@ 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;
|
||||
};
|
||||
|
|
@ -627,6 +660,22 @@ 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']>,
|
||||
|
|
@ -1230,6 +1279,10 @@ export const {
|
|||
changeTimezone,
|
||||
changeSatelliteRegistrationCommand,
|
||||
changeSatelliteCaCertificate,
|
||||
changeAapCallbackUrl,
|
||||
changeAapHostConfigKey,
|
||||
changeAapTlsCertificateAuthority,
|
||||
changeAapTlsConfirmation,
|
||||
addNtpServer,
|
||||
removeNtpServer,
|
||||
changeHostname,
|
||||
|
|
|
|||
|
|
@ -714,6 +714,9 @@ describe('Import modal', () => {
|
|||
),
|
||||
);
|
||||
|
||||
// AAP
|
||||
await clickNext();
|
||||
|
||||
// Firstboot
|
||||
await clickNext();
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -332,4 +332,30 @@ 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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -513,6 +513,123 @@ 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -109,12 +109,12 @@ describe('Step Services', () => {
|
|||
router = undefined;
|
||||
});
|
||||
|
||||
test('clicking Next loads First boot script', async () => {
|
||||
test('clicking Next loads Ansible Automation Platform', async () => {
|
||||
await renderCreateMode();
|
||||
await goToServicesStep();
|
||||
await clickNext();
|
||||
await screen.findByRole('heading', {
|
||||
name: 'First boot configuration',
|
||||
name: 'Ansible Automation Platform',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
14
src/test/fixtures/compliance.ts
vendored
14
src/test/fixtures/compliance.ts
vendored
|
|
@ -36,8 +36,20 @@ 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: 3,
|
||||
total: 4,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
16
src/test/fixtures/oscap.ts
vendored
16
src/test/fixtures/oscap.ts
vendored
|
|
@ -124,9 +124,17 @@ export const oscapCustomizationsPolicy = (
|
|||
): GetOscapCustomizationsApiResponse => {
|
||||
const policyData = mockPolicies.data.find((p) => p.id === policy);
|
||||
const customizations = oscapCustomizations(policyData!.ref_id);
|
||||
// filter out a single package to simulate the customizations being tailored
|
||||
customizations.packages = customizations.packages!.filter(
|
||||
(p) => p !== 'aide',
|
||||
);
|
||||
|
||||
// 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',
|
||||
);
|
||||
}
|
||||
|
||||
return customizations;
|
||||
};
|
||||
|
|
|
|||
58
src/test/fixtures/packages.ts
vendored
58
src/test/fixtures/packages.ts
vendored
|
|
@ -75,6 +75,64 @@ 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 [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue