Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f1ba74759 | ||
|
|
1c3ed83889 | ||
|
|
59f500f5c8 |
57 changed files with 1876 additions and 2818 deletions
|
|
@ -1,257 +0,0 @@
|
||||||
---
|
|
||||||
name: Debian Image Builder Frontend CI/CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, develop]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_VERSION: "18"
|
|
||||||
DEBIAN_FRONTEND: noninteractive
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-test:
|
|
||||||
name: Build and Test Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build || echo "Build script not found"
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run test; then
|
|
||||||
npm test
|
|
||||||
else
|
|
||||||
echo "No test script found, skipping tests"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run linting
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run lint; then
|
|
||||||
npm run lint
|
|
||||||
else
|
|
||||||
echo "No lint script found, skipping linting"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: frontend-build
|
|
||||||
path: |
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
package:
|
|
||||||
name: Package Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
devscripts \
|
|
||||||
debhelper \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create debian directory
|
|
||||||
run: |
|
|
||||||
mkdir -p debian
|
|
||||||
cat > debian/control << EOF
|
|
||||||
Source: debian-image-builder-frontend
|
|
||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Debian Forge Team <team@debian-forge.org>
|
|
||||||
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
|
||||||
Standards-Version: 4.6.2
|
|
||||||
|
|
||||||
Package: debian-image-builder-frontend
|
|
||||||
Architecture: all
|
|
||||||
Depends: \${misc:Depends}, nodejs, nginx
|
|
||||||
Description: Debian Image Builder Frontend
|
|
||||||
Web-based frontend for Debian Image Builder with Cockpit integration.
|
|
||||||
Provides a user interface for managing image builds, blueprints,
|
|
||||||
and system configurations through a modern React application.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/rules << EOF
|
|
||||||
#!/usr/bin/make -f
|
|
||||||
%:
|
|
||||||
dh \$@
|
|
||||||
|
|
||||||
override_dh_auto_install:
|
|
||||||
dh_auto_install
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
|
||||||
|
|
||||||
# Copy built frontend files
|
|
||||||
if [ -d dist ]; then
|
|
||||||
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
elif [ -d build ]; then
|
|
||||||
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy source files for development
|
|
||||||
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
|
|
||||||
# Create nginx configuration
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /usr/share/debian-image-builder-frontend;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://localhost:8080/;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX_EOF
|
|
||||||
|
|
||||||
# Create cockpit manifest
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
|
||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"manifest": {
|
|
||||||
"name": "debian-image-builder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"title": "Debian Image Builder",
|
|
||||||
"description": "Build and manage Debian atomic images",
|
|
||||||
"url": "/usr/share/debian-image-builder-frontend",
|
|
||||||
"icon": "debian-logo",
|
|
||||||
"requires": {
|
|
||||||
"cockpit": ">= 200"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
COCKPIT_EOF
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/changelog << EOF
|
|
||||||
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Initial release
|
|
||||||
* Debian Image Builder Frontend with Cockpit integration
|
|
||||||
* React-based web interface for image management
|
|
||||||
|
|
||||||
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/compat << EOF
|
|
||||||
13
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x debian/rules
|
|
||||||
|
|
||||||
- name: Build Debian package
|
|
||||||
run: |
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
ls -la ../*.deb
|
|
||||||
|
|
||||||
- name: Upload Debian package
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: debian-image-builder-frontend-deb
|
|
||||||
path: ../*.deb
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
cockpit-integration:
|
|
||||||
name: Test Cockpit Integration
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Test cockpit integration
|
|
||||||
run: |
|
|
||||||
echo "Testing Cockpit integration..."
|
|
||||||
if [ -d cockpit ]; then
|
|
||||||
echo "Cockpit directory found:"
|
|
||||||
ls -la cockpit/
|
|
||||||
else
|
|
||||||
echo "No cockpit directory found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f package.json ]; then
|
|
||||||
echo "Package.json scripts:"
|
|
||||||
npm run
|
|
||||||
fi
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
---
|
|
||||||
name: Debian Image Builder Frontend CI/CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, develop]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_VERSION: "18"
|
|
||||||
DEBIAN_FRONTEND: noninteractive
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-test:
|
|
||||||
name: Build and Test Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build || echo "Build script not found"
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run test; then
|
|
||||||
npm test
|
|
||||||
else
|
|
||||||
echo "No test script found, skipping tests"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run linting
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run lint; then
|
|
||||||
npm run lint
|
|
||||||
else
|
|
||||||
echo "No lint script found, skipping linting"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: frontend-build
|
|
||||||
path: |
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
package:
|
|
||||||
name: Package Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
devscripts \
|
|
||||||
debhelper \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create debian directory
|
|
||||||
run: |
|
|
||||||
mkdir -p debian
|
|
||||||
cat > debian/control << EOF
|
|
||||||
Source: debian-image-builder-frontend
|
|
||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Debian Forge Team <team@debian-forge.org>
|
|
||||||
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
|
||||||
Standards-Version: 4.6.2
|
|
||||||
|
|
||||||
Package: debian-image-builder-frontend
|
|
||||||
Architecture: all
|
|
||||||
Depends: \${misc:Depends}, nodejs, nginx
|
|
||||||
Description: Debian Image Builder Frontend
|
|
||||||
Web-based frontend for Debian Image Builder with Cockpit integration.
|
|
||||||
Provides a user interface for managing image builds, blueprints,
|
|
||||||
and system configurations through a modern React application.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/rules << EOF
|
|
||||||
#!/usr/bin/make -f
|
|
||||||
%:
|
|
||||||
dh \$@
|
|
||||||
|
|
||||||
override_dh_auto_install:
|
|
||||||
dh_auto_install
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
|
||||||
|
|
||||||
# Copy built frontend files
|
|
||||||
if [ -d dist ]; then
|
|
||||||
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
elif [ -d build ]; then
|
|
||||||
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy source files for development
|
|
||||||
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
|
|
||||||
# Create nginx configuration
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /usr/share/debian-image-builder-frontend;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://localhost:8080/;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX_EOF
|
|
||||||
|
|
||||||
# Create cockpit manifest
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
|
||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"manifest": {
|
|
||||||
"name": "debian-image-builder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"title": "Debian Image Builder",
|
|
||||||
"description": "Build and manage Debian atomic images",
|
|
||||||
"url": "/usr/share/debian-image-builder-frontend",
|
|
||||||
"icon": "debian-logo",
|
|
||||||
"requires": {
|
|
||||||
"cockpit": ">= 200"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
COCKPIT_EOF
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/changelog << EOF
|
|
||||||
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Initial release
|
|
||||||
* Debian Image Builder Frontend with Cockpit integration
|
|
||||||
* React-based web interface for image management
|
|
||||||
|
|
||||||
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/compat << EOF
|
|
||||||
13
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x debian/rules
|
|
||||||
|
|
||||||
- name: Build Debian package
|
|
||||||
run: |
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
ls -la ../*.deb
|
|
||||||
|
|
||||||
- name: Upload Debian package
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: debian-image-builder-frontend-deb
|
|
||||||
path: ../*.deb
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
cockpit-integration:
|
|
||||||
name: Test Cockpit Integration
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Test cockpit integration
|
|
||||||
run: |
|
|
||||||
echo "Testing Cockpit integration..."
|
|
||||||
if [ -d cockpit ]; then
|
|
||||||
echo "Cockpit directory found:"
|
|
||||||
ls -la cockpit/
|
|
||||||
else
|
|
||||||
echo "No cockpit directory found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f package.json ]; then
|
|
||||||
echo "Package.json scripts:"
|
|
||||||
npm run
|
|
||||||
fi
|
|
||||||
|
|
@ -32,7 +32,8 @@ test:
|
||||||
- RUNNER:
|
- RUNNER:
|
||||||
- aws/fedora-41-x86_64
|
- aws/fedora-41-x86_64
|
||||||
- aws/fedora-42-x86_64
|
- aws/fedora-42-x86_64
|
||||||
- aws/rhel-10.1-nightly-x86_64
|
- aws/rhel-9.6-nightly-x86_64
|
||||||
|
- aws/rhel-10.0-nightly-x86_64
|
||||||
INTERNAL_NETWORK: ["true"]
|
INTERNAL_NETWORK: ["true"]
|
||||||
|
|
||||||
finish:
|
finish:
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: prefetch-dependencies
|
value: prefetch-dependencies
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:76bc23ca84c6b31251fee267f417ed262d9ea49655e942d1978149e137295bb0
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -238,7 +238,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: buildah
|
value: buildah
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:09ca1ce263bb686b08187a3b836f6a5bd54875e8596105e4d7d816d57caf55a0
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -413,7 +413,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: ecosystem-cert-preflight-checks
|
value: ecosystem-cert-preflight-checks
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:4bafcaab0f0c998a89a1cc33bdbbf74f39eea52e6c0e43013c356a322f94940f
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -503,7 +503,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: push-dockerfile
|
value: push-dockerfile
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:5446102f233991bdc73451201adf361766d45c638f3d89f19121ae1c2ba8bf17
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: prefetch-dependencies
|
value: prefetch-dependencies
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:76bc23ca84c6b31251fee267f417ed262d9ea49655e942d1978149e137295bb0
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -235,7 +235,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: buildah
|
value: buildah
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:09ca1ce263bb686b08187a3b836f6a5bd54875e8596105e4d7d816d57caf55a0
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -410,7 +410,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: ecosystem-cert-preflight-checks
|
value: ecosystem-cert-preflight-checks
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:4bafcaab0f0c998a89a1cc33bdbbf74f39eea52e6c0e43013c356a322f94940f
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -500,7 +500,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: push-dockerfile
|
value: push-dockerfile
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:5446102f233991bdc73451201adf361766d45c638f3d89f19121ae1c2ba8bf17
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Name: cockpit-image-builder
|
Name: cockpit-image-builder
|
||||||
Version: 76
|
Version: 74
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Image builder plugin for Cockpit
|
Summary: Image builder plugin for Cockpit
|
||||||
|
|
||||||
|
|
|
||||||
1998
package-lock.json
generated
1998
package-lock.json
generated
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
|
@ -8,17 +8,16 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ltd/j-toml": "1.38.0",
|
"@ltd/j-toml": "1.38.0",
|
||||||
"@patternfly/patternfly": "6.3.1",
|
"@patternfly/patternfly": "6.3.0",
|
||||||
"@patternfly/react-code-editor": "6.3.1",
|
"@patternfly/react-code-editor": "6.3.1",
|
||||||
"@patternfly/react-core": "6.3.1",
|
"@patternfly/react-core": "6.3.0",
|
||||||
"@patternfly/react-table": "6.3.1",
|
"@patternfly/react-table": "6.3.1",
|
||||||
"@redhat-cloud-services/frontend-components": "7.0.3",
|
"@redhat-cloud-services/frontend-components": "7.0.3",
|
||||||
"@redhat-cloud-services/frontend-components-notifications": "6.1.5",
|
"@redhat-cloud-services/frontend-components-notifications": "6.1.3",
|
||||||
"@redhat-cloud-services/frontend-components-utilities": "7.0.3",
|
"@redhat-cloud-services/frontend-components-utilities": "7.0.3",
|
||||||
"@redhat-cloud-services/types": "3.0.1",
|
|
||||||
"@reduxjs/toolkit": "2.8.2",
|
"@reduxjs/toolkit": "2.8.2",
|
||||||
"@scalprum/react-core": "0.9.5",
|
"@scalprum/react-core": "0.9.5",
|
||||||
"@sentry/webpack-plugin": "4.1.1",
|
"@sentry/webpack-plugin": "4.1.0",
|
||||||
"@unleash/proxy-client-react": "5.0.1",
|
"@unleash/proxy-client-react": "5.0.1",
|
||||||
"classnames": "2.5.1",
|
"classnames": "2.5.1",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
|
|
@ -47,13 +46,13 @@
|
||||||
"@testing-library/jest-dom": "6.6.4",
|
"@testing-library/jest-dom": "6.6.4",
|
||||||
"@testing-library/react": "16.3.0",
|
"@testing-library/react": "16.3.0",
|
||||||
"@testing-library/user-event": "14.6.1",
|
"@testing-library/user-event": "14.6.1",
|
||||||
"@types/node": "24.3.0",
|
"@types/node": "24.1.0",
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "18.3.12",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "18.3.1",
|
||||||
"@types/react-redux": "7.1.34",
|
"@types/react-redux": "7.1.34",
|
||||||
"@types/uuid": "10.0.0",
|
"@types/uuid": "10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
"@typescript-eslint/eslint-plugin": "8.39.0",
|
||||||
"@typescript-eslint/parser": "8.40.0",
|
"@typescript-eslint/parser": "8.39.0",
|
||||||
"@vitejs/plugin-react": "4.7.0",
|
"@vitejs/plugin-react": "4.7.0",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"babel-loader": "10.0.0",
|
"babel-loader": "10.0.0",
|
||||||
|
|
@ -62,13 +61,13 @@
|
||||||
"chartjs-plugin-annotation": "3.1.0",
|
"chartjs-plugin-annotation": "3.1.0",
|
||||||
"copy-webpack-plugin": "13.0.0",
|
"copy-webpack-plugin": "13.0.0",
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"eslint": "9.33.0",
|
"eslint": "9.32.0",
|
||||||
"eslint-plugin-disable-autofix": "5.0.1",
|
"eslint-plugin-disable-autofix": "5.0.1",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-jest-dom": "5.5.0",
|
"eslint-plugin-jest-dom": "5.5.0",
|
||||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||||
"eslint-plugin-playwright": "2.2.2",
|
"eslint-plugin-playwright": "2.2.2",
|
||||||
"eslint-plugin-prettier": "5.5.4",
|
"eslint-plugin-prettier": "5.5.3",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"eslint-plugin-react-hooks": "5.2.0",
|
"eslint-plugin-react-hooks": "5.2.0",
|
||||||
"eslint-plugin-react-redux": "4.2.2",
|
"eslint-plugin-react-redux": "4.2.2",
|
||||||
|
|
@ -81,7 +80,7 @@
|
||||||
"madge": "8.0.0",
|
"madge": "8.0.0",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"msw": "2.10.5",
|
"msw": "2.10.4",
|
||||||
"npm-run-all": "4.1.5",
|
"npm-run-all": "4.1.5",
|
||||||
"path-browserify": "1.0.1",
|
"path-browserify": "1.0.1",
|
||||||
"postcss-scss": "4.0.9",
|
"postcss-scss": "4.0.9",
|
||||||
|
|
@ -89,12 +88,12 @@
|
||||||
"redux-mock-store": "1.5.5",
|
"redux-mock-store": "1.5.5",
|
||||||
"sass": "1.90.0",
|
"sass": "1.90.0",
|
||||||
"sass-loader": "16.0.5",
|
"sass-loader": "16.0.5",
|
||||||
"stylelint": "16.23.1",
|
"stylelint": "16.23.0",
|
||||||
"stylelint-config-recommended-scss": "16.0.0",
|
"stylelint-config-recommended-scss": "15.0.1",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"ts-patch": "3.3.0",
|
"ts-patch": "3.3.0",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"typescript-eslint": "8.40.0",
|
"typescript-eslint": "8.38.0",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"vitest": "3.2.4",
|
"vitest": "3.2.4",
|
||||||
"vitest-canvas-mock": "0.3.3",
|
"vitest-canvas-mock": "0.3.3",
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ jobs:
|
||||||
tmt_plan: /plans/all/main
|
tmt_plan: /plans/all/main
|
||||||
targets:
|
targets:
|
||||||
- centos-stream-10
|
- centos-stream-10
|
||||||
|
- centos-stream-10-aarch64
|
||||||
- fedora-41
|
- fedora-41
|
||||||
- fedora-42
|
- fedora-42
|
||||||
|
- fedora-latest-stable-aarch64
|
||||||
|
|
||||||
- job: copr_build
|
- job: copr_build
|
||||||
trigger: pull_request
|
trigger: pull_request
|
||||||
|
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { test } from '../fixtures/customizations';
|
|
||||||
import { isHosted } from '../helpers/helpers';
|
|
||||||
import { ensureAuthenticated } from '../helpers/login';
|
|
||||||
import {
|
|
||||||
ibFrame,
|
|
||||||
navigateToLandingPage,
|
|
||||||
navigateToOptionalSteps,
|
|
||||||
} from '../helpers/navHelpers';
|
|
||||||
import {
|
|
||||||
createBlueprint,
|
|
||||||
deleteBlueprint,
|
|
||||||
exportBlueprint,
|
|
||||||
fillInDetails,
|
|
||||||
fillInImageOutputGuest,
|
|
||||||
importBlueprint,
|
|
||||||
registerLater,
|
|
||||||
} from '../helpers/wizardHelpers';
|
|
||||||
|
|
||||||
const validCallbackUrl =
|
|
||||||
'https://controller.url/api/controller/v2/job_templates/9/callback/';
|
|
||||||
const validHttpCallbackUrl =
|
|
||||||
'http://controller.url/api/controller/v2/job_templates/9/callback/';
|
|
||||||
const validHostConfigKey = 'hostconfigkey';
|
|
||||||
const validCertificate = `-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDXTCCAkWgAwIBAgIJAOEzx5ezZ9EIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
|
||||||
BAYTAklOMQswCQYDVQQIDAJLUjEMMAoGA1UEBwwDS1JHMRAwDgYDVQQKDAdUZXN0
|
|
||||||
IENBMB4XDTI1MDUxNTEyMDAwMFoXDTI2MDUxNTEyMDAwMFowRTELMAkGA1UEBhMC
|
|
||||||
SU4xCzAJBgNVBAgMAktSMQwwCgYDVQQHDANSR0sxEDAOBgNVBAoMB1Rlc3QgQ0Ew
|
|
||||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+R4gfN5pyJQo5qBTTtN+7
|
|
||||||
eE9CSXZJ8SVVaE3U54IgqQoqsSoBY5QtExy7v5C6l6mW4E6dzK/JecmvTTO/BvlG
|
|
||||||
A5k2hxB6bOQxtxYwfgElH+RFWN9P4xxhtEiQgHoG1rDfnXuDJk1U3YEkCQELUebz
|
|
||||||
fF3EIDU1yR0Sz2bA+Sl2VXe8og1MEZfytq8VZUVltxtn2PfW7zI5gOllBR2sKeUc
|
|
||||||
K6h8HXN7qMgfEvsLIXxTw7fU/zA3ibcxfRCl3m6QhF8hwRh6F9Wtz2s8hCzGegV5
|
|
||||||
z0M39nY7X8C3GZQ4Ly8v8DdY+FbEix7K3SSBRbWtdPfAHRFlX9Er2Wf8DAr7O2hH
|
|
||||||
AgMBAAGjUDBOMB0GA1UdDgQWBBTXXz2eIDgK+BhzDUAGzptn0OMcpDAfBgNVHSME
|
|
||||||
GDAWgBTXXz2eIDgK+BhzDUAGzptn0OMcpDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
|
|
||||||
DQEBCwUAA4IBAQAoUgY4jsuBMB3el9cc7JS2rcOhhJzn47Hj2UANfJq52g5lbjo7
|
|
||||||
XDc7Wb3VDcV+1LzjdzayT1qO1WzHb6FDPW9L9f6h4s8lj6MvJ+xhOWgD11srdIt3
|
|
||||||
vbQaQW4zDfeVRcKXzqbcUX8BLXAdzJPqVwZ+Z4EDjYrJ7lF9k+IqfZm0MsYX7el9
|
|
||||||
kvdRHbLuF4Q0sZ05CXMFkhM0Ulhu4MZ+1FcsQa7nWfZzTmbjHOuWJPB4z5WwrB7z
|
|
||||||
U8YYvWJ3qxToWGbATqJxkRKGGqLrNrmwcfzgPqkpuCRYi0Kky6gJ1RvL+DRopY9x
|
|
||||||
uD+ckf3oH2wYAB6RpPRMkfVxe7lGMvq/yEZ6
|
|
||||||
-----END CERTIFICATE-----`;
|
|
||||||
const invalidCertificate = `-----BEGIN CERTIFICATE-----
|
|
||||||
ThisIs*Not+Valid/Base64==
|
|
||||||
-----END CERTIFICATE-----`;
|
|
||||||
|
|
||||||
test('Create a blueprint with AAP registration customization', async ({
|
|
||||||
page,
|
|
||||||
cleanup,
|
|
||||||
}) => {
|
|
||||||
const blueprintName = 'test-' + uuidv4();
|
|
||||||
|
|
||||||
// Skip entirely in Cockpit/on-premise where AAP customization is unavailable
|
|
||||||
test.skip(!isHosted(), 'AAP customization is not available in the plugin');
|
|
||||||
|
|
||||||
// Delete the blueprint after the run fixture
|
|
||||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
|
||||||
await ensureAuthenticated(page);
|
|
||||||
|
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
|
||||||
|
|
||||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
|
||||||
await navigateToOptionalSteps(frame);
|
|
||||||
await registerLater(frame);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Select and fill the AAP step with valid configuration', async () => {
|
|
||||||
await frame
|
|
||||||
.getByRole('button', { name: 'Ansible Automation Platform' })
|
|
||||||
.click();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'ansible callback url' })
|
|
||||||
.fill(validCallbackUrl);
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'host config key' })
|
|
||||||
.fill(validHostConfigKey);
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'File upload' })
|
|
||||||
.fill(validCertificate);
|
|
||||||
await expect(frame.getByRole('button', { name: 'Next' })).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Test TLS confirmation checkbox for HTTPS URLs', async () => {
|
|
||||||
// TLS confirmation checkbox should appear for HTTPS URLs
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('checkbox', {
|
|
||||||
name: 'Insecure',
|
|
||||||
}),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
// Check TLS confirmation and verify CA input is hidden
|
|
||||||
await frame
|
|
||||||
.getByRole('checkbox', {
|
|
||||||
name: 'Insecure',
|
|
||||||
})
|
|
||||||
.check();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toBeHidden();
|
|
||||||
|
|
||||||
await frame
|
|
||||||
.getByRole('checkbox', {
|
|
||||||
name: 'Insecure',
|
|
||||||
})
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Test certificate validation', async () => {
|
|
||||||
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'File upload' })
|
|
||||||
.fill(invalidCertificate);
|
|
||||||
await expect(frame.getByText(/Certificate.*is not valid/)).toBeVisible();
|
|
||||||
|
|
||||||
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'File upload' })
|
|
||||||
.fill(validCertificate);
|
|
||||||
|
|
||||||
await expect(frame.getByText('Certificate was uploaded')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Test HTTP URL behavior', async () => {
|
|
||||||
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'ansible callback url' })
|
|
||||||
.fill(validHttpCallbackUrl);
|
|
||||||
|
|
||||||
// TLS confirmation checkbox should NOT appear for HTTP URLs
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('checkbox', {
|
|
||||||
name: 'Insecure',
|
|
||||||
}),
|
|
||||||
).toBeHidden();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'ansible callback url' })
|
|
||||||
.fill(validCallbackUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Complete AAP configuration and proceed to review', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Fill the BP details', async () => {
|
|
||||||
await fillInDetails(frame, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Create BP', async () => {
|
|
||||||
await createBlueprint(frame, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Edit BP and verify AAP configuration persists', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
|
||||||
await frame.getByLabel('Revisit Ansible Automation Platform step').click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'ansible callback url' }),
|
|
||||||
).toHaveValue(validCallbackUrl);
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'host config key' }),
|
|
||||||
).toHaveValue(validHostConfigKey);
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toHaveValue(validCertificate);
|
|
||||||
|
|
||||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
|
||||||
await frame
|
|
||||||
.getByRole('button', { name: 'Save changes to blueprint' })
|
|
||||||
.click();
|
|
||||||
});
|
|
||||||
// This is for hosted service only as these features are not available in cockpit plugin
|
|
||||||
await test.step('Export BP', async (step) => {
|
|
||||||
step.skip(!isHosted(), 'Exporting is not available in the plugin');
|
|
||||||
await exportBlueprint(page, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Import BP', async (step) => {
|
|
||||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
|
||||||
await importBlueprint(page, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Review imported BP', async (step) => {
|
|
||||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
|
||||||
await fillInImageOutputGuest(page);
|
|
||||||
await page
|
|
||||||
.getByRole('button', { name: 'Ansible Automation Platform' })
|
|
||||||
.click();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('textbox', { name: 'ansible callback url' }),
|
|
||||||
).toHaveValue(validCallbackUrl);
|
|
||||||
await expect(
|
|
||||||
page.getByRole('textbox', { name: 'host config key' }),
|
|
||||||
).toBeEmpty();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toHaveValue(validCertificate);
|
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -67,10 +67,8 @@ export const getHostDistroName = (): string => {
|
||||||
const lineData = l.split('=');
|
const lineData = l.split('=');
|
||||||
(osRel as any)[lineData[0]] = lineData[1].replace(/"/g, '');
|
(osRel as any)[lineData[0]] = lineData[1].replace(/"/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip minor version from rhel
|
|
||||||
const distro = ON_PREM_RELEASES.get(
|
const distro = ON_PREM_RELEASES.get(
|
||||||
`${(osRel as any)['ID']}-${(osRel as any)['VERSION_ID'].split('.')[0]}`,
|
`${(osRel as any)['ID']}-${(osRel as any)['VERSION_ID']}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (distro === undefined) {
|
if (distro === undefined) {
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,6 @@ test.describe.serial('test', () => {
|
||||||
frame.getByRole('heading', { name: 'Systemd services' });
|
frame.getByRole('heading', { name: 'Systemd services' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
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()) {
|
if (isHosted()) {
|
||||||
frame.getByRole('heading', { name: 'First boot configuration' });
|
frame.getByRole('heading', { name: 'First boot configuration' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
@ -92,12 +87,7 @@ test.describe.serial('test', () => {
|
||||||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
frame.locator('.pf-v6-c-card__title-text').getByText(
|
frame.locator('.pf-v6-c-card__title-text').getByText(blueprintName),
|
||||||
// if the name is too long, the blueprint card will have a truncated name.
|
|
||||||
blueprintName.length > 24
|
|
||||||
? blueprintName.slice(0, 24) + '...'
|
|
||||||
: blueprintName,
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
cf0a810fd3b75fa27139746c4dfe72222e13dcba
|
7b4735d287dd0950e0a6f47dde65b62b0f239da1
|
||||||
|
|
|
||||||
|
|
@ -50,21 +50,11 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
|
||||||
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardTitle aria-label={blueprint.name}>
|
<CardTitle>
|
||||||
{isLoading && blueprint.id === selectedBlueprintId && (
|
{isLoading && blueprint.id === selectedBlueprintId && (
|
||||||
<Spinner size='md' />
|
<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>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>{blueprint.description}</CardBody>
|
<CardBody>{blueprint.description}</CardBody>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Bullseye,
|
Bullseye,
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
||||||
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
@ -28,7 +29,6 @@ import {
|
||||||
PAGINATION_LIMIT,
|
PAGINATION_LIMIT,
|
||||||
PAGINATION_OFFSET,
|
PAGINATION_OFFSET,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useGetUser } from '../../Hooks';
|
|
||||||
import { useGetBlueprintsQuery } from '../../store/backendApi';
|
import { useGetBlueprintsQuery } from '../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
selectBlueprintSearchInput,
|
selectBlueprintSearchInput,
|
||||||
|
|
@ -60,8 +60,8 @@ type emptyBlueprintStateProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const BlueprintsSidebar = () => {
|
const BlueprintsSidebar = () => {
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
|
||||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||||
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
||||||
|
|
@ -73,6 +73,16 @@ const BlueprintsSidebar = () => {
|
||||||
offset: blueprintsOffset,
|
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) {
|
if (blueprintSearchInput) {
|
||||||
searchParams.search = blueprintSearchInput;
|
searchParams.search = blueprintSearchInput;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -16,13 +16,11 @@ import {
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
|
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
|
|
||||||
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
|
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
|
||||||
import {
|
import { useComposeBPWithNotification as useComposeBlueprintMutation } from '../../Hooks';
|
||||||
useComposeBPWithNotification as useComposeBlueprintMutation,
|
|
||||||
useGetUser,
|
|
||||||
} from '../../Hooks';
|
|
||||||
import { useGetBlueprintQuery } from '../../store/backendApi';
|
import { useGetBlueprintQuery } from '../../store/backendApi';
|
||||||
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
|
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
|
||||||
import { useAppSelector } from '../../store/hooks';
|
import { useAppSelector } from '../../store/hooks';
|
||||||
|
|
@ -39,7 +37,18 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
||||||
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
||||||
useComposeBlueprintMutation();
|
useComposeBlueprintMutation();
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onBuildHandler = async () => {
|
const onBuildHandler = async () => {
|
||||||
if (selectedBlueprintId) {
|
if (selectedBlueprintId) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -9,16 +9,14 @@ import {
|
||||||
ModalVariant,
|
ModalVariant,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AMPLITUDE_MODULE_NAME,
|
AMPLITUDE_MODULE_NAME,
|
||||||
PAGINATION_LIMIT,
|
PAGINATION_LIMIT,
|
||||||
PAGINATION_OFFSET,
|
PAGINATION_OFFSET,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import {
|
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
|
||||||
useDeleteBPWithNotification as useDeleteBlueprintMutation,
|
|
||||||
useGetUser,
|
|
||||||
} from '../../Hooks';
|
|
||||||
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
selectBlueprintSearchInput,
|
selectBlueprintSearchInput,
|
||||||
|
|
@ -44,7 +42,17 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
||||||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const searchParams: GetBlueprintsApiArg = {
|
const searchParams: GetBlueprintsApiArg = {
|
||||||
limit: blueprintsLimit,
|
limit: blueprintsLimit,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import { WizardStepType } from '@patternfly/react-core/dist/esm/components/Wizar
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import AAPStep from './steps/AAP';
|
|
||||||
import DetailsStep from './steps/Details';
|
import DetailsStep from './steps/Details';
|
||||||
import FileSystemStep from './steps/FileSystem';
|
import FileSystemStep from './steps/FileSystem';
|
||||||
import { FileSystemContext } from './steps/FileSystem/components/FileSystemTable';
|
import { FileSystemContext } from './steps/FileSystem/components/FileSystemTable';
|
||||||
|
|
@ -41,7 +40,6 @@ import UsersStep from './steps/Users';
|
||||||
import { getHostArch, getHostDistro } from './utilities/getHostInfo';
|
import { getHostArch, getHostDistro } from './utilities/getHostInfo';
|
||||||
import { useHasSpecificTargetOnly } from './utilities/hasSpecificTargetOnly';
|
import { useHasSpecificTargetOnly } from './utilities/hasSpecificTargetOnly';
|
||||||
import {
|
import {
|
||||||
useAAPValidation,
|
|
||||||
useDetailsValidation,
|
useDetailsValidation,
|
||||||
useFilesystemValidation,
|
useFilesystemValidation,
|
||||||
useFirewallValidation,
|
useFirewallValidation,
|
||||||
|
|
@ -199,7 +197,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
|
|
||||||
// Feature flags
|
// Feature flags
|
||||||
const complianceEnabled = useFlag('image-builder.compliance.enabled');
|
const complianceEnabled = useFlag('image-builder.compliance.enabled');
|
||||||
const isAAPRegistrationEnabled = useFlag('image-builder.aap.enabled');
|
|
||||||
|
|
||||||
// IMPORTANT: Ensure the wizard starts with a fresh initial state
|
// IMPORTANT: Ensure the wizard starts with a fresh initial state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -286,8 +283,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
const firewallValidation = useFirewallValidation();
|
const firewallValidation = useFirewallValidation();
|
||||||
// Services
|
// Services
|
||||||
const servicesValidation = useServicesValidation();
|
const servicesValidation = useServicesValidation();
|
||||||
// AAP
|
|
||||||
const aapValidation = useAAPValidation();
|
|
||||||
// Firstboot
|
// Firstboot
|
||||||
const firstBootValidation = useFirstBootValidation();
|
const firstBootValidation = useFirstBootValidation();
|
||||||
// Details
|
// Details
|
||||||
|
|
@ -298,10 +293,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
const hasWslTargetOnly = useHasSpecificTargetOnly('wsl');
|
const hasWslTargetOnly = useHasSpecificTargetOnly('wsl');
|
||||||
|
|
||||||
let startIndex = 1; // default index
|
let startIndex = 1; // default index
|
||||||
const JUMP_TO_REVIEW_STEP = 23;
|
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
startIndex = JUMP_TO_REVIEW_STEP;
|
startIndex = 22;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
|
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
|
||||||
|
|
@ -662,22 +655,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
>
|
>
|
||||||
<ServicesStep />
|
<ServicesStep />
|
||||||
</WizardStep>,
|
</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
|
<WizardStep
|
||||||
name='First boot script configuration'
|
name='First boot script configuration'
|
||||||
id='wizard-first-boot'
|
id='wizard-first-boot'
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ type ValidatedTextInputPropTypes = TextInputProps & {
|
||||||
type ValidationInputProp = TextInputProps &
|
type ValidationInputProp = TextInputProps &
|
||||||
TextAreaProps & {
|
TextAreaProps & {
|
||||||
value: string;
|
value: string;
|
||||||
placeholder?: string;
|
placeholder: string;
|
||||||
stepValidation: StepValidation;
|
stepValidation: StepValidation;
|
||||||
dataTestId?: string;
|
dataTestId?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
|
@ -91,7 +91,7 @@ export const ValidatedInputAndTextArea = ({
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
validated={validated}
|
validated={validated}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
placeholder={placeholder || ''}
|
placeholder={placeholder}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
/>
|
/>
|
||||||
|
|
@ -138,7 +138,6 @@ export const ValidatedInput = ({
|
||||||
value,
|
value,
|
||||||
placeholder,
|
placeholder,
|
||||||
onChange,
|
onChange,
|
||||||
...props
|
|
||||||
}: ValidatedTextInputPropTypes) => {
|
}: ValidatedTextInputPropTypes) => {
|
||||||
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
||||||
|
|
||||||
|
|
@ -165,7 +164,6 @@ export const ValidatedInput = ({
|
||||||
aria-label={ariaLabel || ''}
|
aria-label={ariaLabel || ''}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
placeholder={placeholder || ''}
|
placeholder={placeholder || ''}
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
{!isPristine && !validator(value) && (
|
{!isPristine && !validator(value) && (
|
||||||
<HelperText>
|
<HelperText>
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
DropEvent,
|
|
||||||
FileUpload,
|
|
||||||
FormGroup,
|
|
||||||
FormHelperText,
|
|
||||||
HelperText,
|
|
||||||
HelperTextItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
|
||||||
import {
|
|
||||||
changeAapCallbackUrl,
|
|
||||||
changeAapHostConfigKey,
|
|
||||||
changeAapTlsCertificateAuthority,
|
|
||||||
changeAapTlsConfirmation,
|
|
||||||
selectAapCallbackUrl,
|
|
||||||
selectAapHostConfigKey,
|
|
||||||
selectAapTlsCertificateAuthority,
|
|
||||||
selectAapTlsConfirmation,
|
|
||||||
} from '../../../../../store/wizardSlice';
|
|
||||||
import { useAAPValidation } from '../../../utilities/useValidation';
|
|
||||||
import { ValidatedInputAndTextArea } from '../../../ValidatedInput';
|
|
||||||
import { validateMultipleCertificates } from '../../../validators';
|
|
||||||
|
|
||||||
const AAPRegistration = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const callbackUrl = useAppSelector(selectAapCallbackUrl);
|
|
||||||
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
|
|
||||||
const tlsCertificateAuthority = useAppSelector(
|
|
||||||
selectAapTlsCertificateAuthority,
|
|
||||||
);
|
|
||||||
const tlsConfirmation = useAppSelector(selectAapTlsConfirmation);
|
|
||||||
const [isRejected, setIsRejected] = React.useState(false);
|
|
||||||
const stepValidation = useAAPValidation();
|
|
||||||
|
|
||||||
const isHttpsUrl = callbackUrl?.toLowerCase().startsWith('https://') || false;
|
|
||||||
const shouldShowCaInput = !isHttpsUrl || (isHttpsUrl && !tlsConfirmation);
|
|
||||||
|
|
||||||
const validated = stepValidation.errors['certificate']
|
|
||||||
? 'error'
|
|
||||||
: stepValidation.errors['certificate'] === undefined &&
|
|
||||||
tlsCertificateAuthority &&
|
|
||||||
validateMultipleCertificates(tlsCertificateAuthority).validCertificates
|
|
||||||
.length > 0
|
|
||||||
? 'success'
|
|
||||||
: 'default';
|
|
||||||
|
|
||||||
const handleCallbackUrlChange = (value: string) => {
|
|
||||||
dispatch(changeAapCallbackUrl(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHostConfigKeyChange = (value: string) => {
|
|
||||||
dispatch(changeAapHostConfigKey(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClear = () => {
|
|
||||||
dispatch(changeAapTlsCertificateAuthority(''));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTextChange = (
|
|
||||||
_event: React.ChangeEvent<HTMLTextAreaElement>,
|
|
||||||
value: string,
|
|
||||||
) => {
|
|
||||||
dispatch(changeAapTlsCertificateAuthority(value));
|
|
||||||
setIsRejected(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDataChange = (_: DropEvent, value: string) => {
|
|
||||||
dispatch(changeAapTlsCertificateAuthority(value));
|
|
||||||
setIsRejected(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileRejected = () => {
|
|
||||||
dispatch(changeAapTlsCertificateAuthority(''));
|
|
||||||
setIsRejected(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTlsConfirmationChange = (checked: boolean) => {
|
|
||||||
dispatch(changeAapTlsConfirmation(checked));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FormGroup label='Ansible Callback URL' isRequired>
|
|
||||||
<ValidatedInputAndTextArea
|
|
||||||
value={callbackUrl || ''}
|
|
||||||
onChange={(_event, value) => handleCallbackUrlChange(value.trim())}
|
|
||||||
ariaLabel='ansible callback url'
|
|
||||||
isRequired
|
|
||||||
stepValidation={stepValidation}
|
|
||||||
fieldName='callbackUrl'
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup label='Host Config Key' isRequired>
|
|
||||||
<ValidatedInputAndTextArea
|
|
||||||
value={hostConfigKey || ''}
|
|
||||||
onChange={(_event, value) => handleHostConfigKeyChange(value.trim())}
|
|
||||||
ariaLabel='host config key'
|
|
||||||
isRequired
|
|
||||||
stepValidation={stepValidation}
|
|
||||||
fieldName='hostConfigKey'
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{shouldShowCaInput && (
|
|
||||||
<FormGroup label='Certificate authority (CA) for Ansible Controller'>
|
|
||||||
<FileUpload
|
|
||||||
id='aap-certificate-upload'
|
|
||||||
type='text'
|
|
||||||
value={tlsCertificateAuthority || ''}
|
|
||||||
filename={tlsCertificateAuthority ? 'CA detected' : ''}
|
|
||||||
onDataChange={handleDataChange}
|
|
||||||
onTextChange={handleTextChange}
|
|
||||||
onClearClick={handleClear}
|
|
||||||
dropzoneProps={{
|
|
||||||
accept: {
|
|
||||||
'application/x-pem-file': ['.pem'],
|
|
||||||
'application/x-x509-ca-cert': ['.cer', '.crt'],
|
|
||||||
'application/pkix-cert': ['.der'],
|
|
||||||
},
|
|
||||||
maxSize: 512000,
|
|
||||||
onDropRejected: handleFileRejected,
|
|
||||||
}}
|
|
||||||
validated={isRejected ? 'error' : validated}
|
|
||||||
browseButtonText='Upload'
|
|
||||||
allowEditingUploadedText={true}
|
|
||||||
/>
|
|
||||||
<FormHelperText>
|
|
||||||
<HelperText>
|
|
||||||
<HelperTextItem
|
|
||||||
variant={
|
|
||||||
isRejected || validated === 'error'
|
|
||||||
? 'error'
|
|
||||||
: validated === 'success'
|
|
||||||
? 'success'
|
|
||||||
: 'default'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isRejected
|
|
||||||
? 'Must be a .PEM/.CER/.CRT file'
|
|
||||||
: validated === 'error'
|
|
||||||
? stepValidation.errors['certificate']
|
|
||||||
: validated === 'success'
|
|
||||||
? 'Certificate was uploaded'
|
|
||||||
: 'Drag and drop a valid certificate file or upload one'}
|
|
||||||
</HelperTextItem>
|
|
||||||
</HelperText>
|
|
||||||
</FormHelperText>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{isHttpsUrl && (
|
|
||||||
<FormGroup>
|
|
||||||
<Checkbox
|
|
||||||
id='tls-confirmation-checkbox'
|
|
||||||
label='Insecure'
|
|
||||||
isChecked={tlsConfirmation || false}
|
|
||||||
onChange={(_event, checked) => handleTlsConfirmationChange(checked)}
|
|
||||||
/>
|
|
||||||
{stepValidation.errors['tlsConfirmation'] && (
|
|
||||||
<FormHelperText>
|
|
||||||
<HelperText>
|
|
||||||
<HelperTextItem variant='error'>
|
|
||||||
{stepValidation.errors['tlsConfirmation']}
|
|
||||||
</HelperTextItem>
|
|
||||||
</HelperText>
|
|
||||||
</FormHelperText>
|
|
||||||
)}
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AAPRegistration;
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Form, Title } from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import AAPRegistration from './components/AAPRegistration';
|
|
||||||
|
|
||||||
const AAPStep = () => {
|
|
||||||
return (
|
|
||||||
<Form>
|
|
||||||
<Title headingLevel='h1' size='xl'>
|
|
||||||
Ansible Automation Platform
|
|
||||||
</Title>
|
|
||||||
<AAPRegistration />
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AAPStep;
|
|
||||||
|
|
@ -8,10 +8,7 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import {
|
import { useGetOscapCustomizationsQuery } from '../../../../../store/backendApi';
|
||||||
useGetComplianceCustomizationsQuery,
|
|
||||||
useGetOscapCustomizationsQuery,
|
|
||||||
} from '../../../../../store/backendApi';
|
|
||||||
import { PolicyRead, usePolicyQuery } from '../../../../../store/complianceApi';
|
import { PolicyRead, usePolicyQuery } from '../../../../../store/complianceApi';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
||||||
import { OpenScapProfile } from '../../../../../store/imageBuilderApi';
|
import { OpenScapProfile } from '../../../../../store/imageBuilderApi';
|
||||||
|
|
@ -19,7 +16,6 @@ import {
|
||||||
changeCompliance,
|
changeCompliance,
|
||||||
selectCompliancePolicyID,
|
selectCompliancePolicyID,
|
||||||
selectComplianceProfileID,
|
selectComplianceProfileID,
|
||||||
selectComplianceType,
|
|
||||||
selectDistribution,
|
selectDistribution,
|
||||||
selectFips,
|
selectFips,
|
||||||
} from '../../../../../store/wizardSlice';
|
} from '../../../../../store/wizardSlice';
|
||||||
|
|
@ -35,29 +31,12 @@ export const OscapProfileInformation = ({
|
||||||
const release = useAppSelector(selectDistribution);
|
const release = useAppSelector(selectDistribution);
|
||||||
const compliancePolicyID = useAppSelector(selectCompliancePolicyID);
|
const compliancePolicyID = useAppSelector(selectCompliancePolicyID);
|
||||||
const complianceProfileID = useAppSelector(selectComplianceProfileID);
|
const complianceProfileID = useAppSelector(selectComplianceProfileID);
|
||||||
const complianceType = useAppSelector(selectComplianceType);
|
|
||||||
const fips = useAppSelector(selectFips);
|
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 {
|
const {
|
||||||
data: oscapProfileInfo,
|
data: oscapProfileInfo,
|
||||||
isFetching: isFetchingOscapProfileInfo,
|
isFetching: isFetchingOscapProfileInfo,
|
||||||
isSuccess: isSuccessOscapProfileInfo,
|
isSuccess: isSuccessOscapProfileInfo,
|
||||||
error: profileError,
|
|
||||||
} = useGetOscapCustomizationsQuery(
|
} = useGetOscapCustomizationsQuery(
|
||||||
{
|
{
|
||||||
distribution: release,
|
distribution: release,
|
||||||
|
|
@ -69,20 +48,6 @@ export const OscapProfileInformation = ({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const customizationData =
|
|
||||||
compliancePolicyID && oscapPolicyInfo ? oscapPolicyInfo : oscapProfileInfo;
|
|
||||||
const profileMetadata = oscapProfileInfo;
|
|
||||||
const isPolicyDataLoading = compliancePolicyID
|
|
||||||
? isFetchingOscapPolicyInfo
|
|
||||||
: false;
|
|
||||||
const isFetchingOscapData = isPolicyDataLoading || isFetchingOscapProfileInfo;
|
|
||||||
const isPolicyDataSuccess = compliancePolicyID
|
|
||||||
? isSuccessOscapPolicyInfo
|
|
||||||
: true;
|
|
||||||
const isSuccessOscapData = isPolicyDataSuccess && isSuccessOscapProfileInfo;
|
|
||||||
const hasCriticalError = profileError || (compliancePolicyID && policyError);
|
|
||||||
const shouldShowData = isSuccessOscapData && !hasCriticalError;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: policyInfo,
|
data: policyInfo,
|
||||||
isFetching: isFetchingPolicyInfo,
|
isFetching: isFetchingPolicyInfo,
|
||||||
|
|
@ -109,28 +74,23 @@ export const OscapProfileInformation = ({
|
||||||
policyTitle: pol.title,
|
policyTitle: pol.title,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}, [isSuccessPolicyInfo, dispatch, policyInfo]);
|
}, [isSuccessPolicyInfo]);
|
||||||
|
|
||||||
const oscapProfile = profileMetadata?.openscap as OpenScapProfile | undefined;
|
const oscapProfile = oscapProfileInfo?.openscap as OpenScapProfile;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(isFetchingOscapData || isFetchingPolicyInfo) && <Spinner size='lg' />}
|
{(isFetchingOscapProfileInfo || isFetchingPolicyInfo) && (
|
||||||
{hasCriticalError && (
|
<Spinner size='lg' />
|
||||||
<Content component={ContentVariants.p} className='pf-v6-u-color-200'>
|
|
||||||
Unable to load compliance information. Please try again.
|
|
||||||
</Content>
|
|
||||||
)}
|
)}
|
||||||
{shouldShowData && (
|
{isSuccessOscapProfileInfo && (
|
||||||
<>
|
<>
|
||||||
<Content component={ContentVariants.dl} className='review-step-dl'>
|
<Content component={ContentVariants.dl} className='review-step-dl'>
|
||||||
<Content
|
<Content
|
||||||
component={ContentVariants.dt}
|
component={ContentVariants.dt}
|
||||||
className='pf-v6-u-min-width'
|
className='pf-v6-u-min-width'
|
||||||
>
|
>
|
||||||
{complianceType === 'compliance'
|
Profile description
|
||||||
? 'Policy description'
|
|
||||||
: 'Profile description'}
|
|
||||||
</Content>
|
</Content>
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
{oscapProfile?.profile_description}
|
{oscapProfile?.profile_description}
|
||||||
|
|
@ -156,7 +116,7 @@ export const OscapProfileInformation = ({
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeBlockCode>
|
<CodeBlockCode>
|
||||||
{(customizationData?.packages ?? []).join(', ')}
|
{(oscapProfileInfo?.packages ?? []).join(', ')}
|
||||||
</CodeBlockCode>
|
</CodeBlockCode>
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
@ -169,7 +129,7 @@ export const OscapProfileInformation = ({
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeBlockCode>
|
<CodeBlockCode>
|
||||||
{customizationData?.kernel?.append}
|
{oscapProfileInfo?.kernel?.append}
|
||||||
</CodeBlockCode>
|
</CodeBlockCode>
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
@ -182,7 +142,7 @@ export const OscapProfileInformation = ({
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeBlockCode>
|
<CodeBlockCode>
|
||||||
{(customizationData?.services?.enabled ?? []).join(' ')}
|
{(oscapProfileInfo?.services?.enabled ?? []).join(' ')}
|
||||||
</CodeBlockCode>
|
</CodeBlockCode>
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
@ -195,8 +155,8 @@ export const OscapProfileInformation = ({
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeBlockCode>
|
<CodeBlockCode>
|
||||||
{(customizationData?.services?.disabled ?? [])
|
{(oscapProfileInfo?.services?.disabled ?? [])
|
||||||
.concat(customizationData?.services?.masked ?? [])
|
.concat(oscapProfileInfo?.services?.masked ?? [])
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
</CodeBlockCode>
|
</CodeBlockCode>
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,15 @@ import {
|
||||||
|
|
||||||
import { useSelectorHandlers } from './useSelectorHandlers';
|
import { useSelectorHandlers } from './useSelectorHandlers';
|
||||||
|
|
||||||
import {
|
|
||||||
useGetComplianceCustomizationsQuery,
|
|
||||||
useLazyGetComplianceCustomizationsQuery,
|
|
||||||
} from '../../../../../store/backendApi';
|
|
||||||
import {
|
import {
|
||||||
PolicyRead,
|
PolicyRead,
|
||||||
usePoliciesQuery,
|
usePoliciesQuery,
|
||||||
} from '../../../../../store/complianceApi';
|
} from '../../../../../store/complianceApi';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
||||||
|
import {
|
||||||
|
useGetOscapCustomizationsForPolicyQuery,
|
||||||
|
useLazyGetOscapCustomizationsForPolicyQuery,
|
||||||
|
} from '../../../../../store/imageBuilderApi';
|
||||||
import {
|
import {
|
||||||
changeCompliance,
|
changeCompliance,
|
||||||
changeFileSystemConfigurationType,
|
changeFileSystemConfigurationType,
|
||||||
|
|
@ -97,7 +97,7 @@ const PolicySelector = () => {
|
||||||
filter: `os_major_version=${majorVersion}`,
|
filter: `os_major_version=${majorVersion}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: currentProfileData } = useGetComplianceCustomizationsQuery(
|
const { data: currentProfileData } = useGetOscapCustomizationsForPolicyQuery(
|
||||||
{
|
{
|
||||||
distribution: release,
|
distribution: release,
|
||||||
policy: policyID!,
|
policy: policyID!,
|
||||||
|
|
@ -105,7 +105,7 @@ const PolicySelector = () => {
|
||||||
{ skip: !policyID },
|
{ skip: !policyID },
|
||||||
);
|
);
|
||||||
|
|
||||||
const [trigger] = useLazyGetComplianceCustomizationsQuery();
|
const [trigger] = useLazyGetOscapCustomizationsForPolicyQuery();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!policies || policies.data === undefined) {
|
if (!policies || policies.data === undefined) {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ import {
|
||||||
Thead,
|
Thead,
|
||||||
Tr,
|
Tr,
|
||||||
} from '@patternfly/react-table';
|
} from '@patternfly/react-table';
|
||||||
import { orderBy } from 'lodash';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import CustomHelperText from './components/CustomHelperText';
|
import CustomHelperText from './components/CustomHelperText';
|
||||||
|
|
@ -67,6 +66,7 @@ import {
|
||||||
} from '../../../../constants';
|
} from '../../../../constants';
|
||||||
import { useGetArchitecturesQuery } from '../../../../store/backendApi';
|
import { useGetArchitecturesQuery } from '../../../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
|
ApiPackageSourcesResponse,
|
||||||
ApiRepositoryResponseRead,
|
ApiRepositoryResponseRead,
|
||||||
ApiSearchRpmResponse,
|
ApiSearchRpmResponse,
|
||||||
useCreateRepositoryMutation,
|
useCreateRepositoryMutation,
|
||||||
|
|
@ -700,7 +700,7 @@ const Packages = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let unpackedData: IBPackageWithRepositoryInfo[] =
|
const unpackedData: IBPackageWithRepositoryInfo[] =
|
||||||
combinedPackageData.flatMap((item) => {
|
combinedPackageData.flatMap((item) => {
|
||||||
// Spread modules into separate rows by application stream
|
// Spread modules into separate rows by application stream
|
||||||
if (item.sources) {
|
if (item.sources) {
|
||||||
|
|
@ -724,16 +724,13 @@ const Packages = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// group by name, but sort by application stream in descending order
|
// group by name, but sort by application stream in descending order
|
||||||
unpackedData = orderBy(
|
unpackedData.sort((a, b) => {
|
||||||
unpackedData,
|
if (a.name === b.name) {
|
||||||
[
|
return (b.stream ?? '').localeCompare(a.stream ?? '');
|
||||||
'name',
|
} else {
|
||||||
(pkg) => pkg.stream || '',
|
return a.name.localeCompare(b.name);
|
||||||
(pkg) => pkg.repository || '',
|
}
|
||||||
(pkg) => pkg.module_name || '',
|
});
|
||||||
],
|
|
||||||
['asc', 'desc', 'asc', 'asc'],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (toggleSelected === 'toggle-available') {
|
if (toggleSelected === 'toggle-available') {
|
||||||
if (activeTabKey === Repos.INCLUDED) {
|
if (activeTabKey === Repos.INCLUDED) {
|
||||||
|
|
@ -869,6 +866,8 @@ const Packages = () => {
|
||||||
dispatch(addPackage(pkg));
|
dispatch(addPackage(pkg));
|
||||||
if (pkg.type === 'module') {
|
if (pkg.type === 'module') {
|
||||||
setActiveStream(pkg.stream || '');
|
setActiveStream(pkg.stream || '');
|
||||||
|
setActiveSortIndex(2);
|
||||||
|
setPage(1);
|
||||||
dispatch(
|
dispatch(
|
||||||
addModule({
|
addModule({
|
||||||
name: pkg.module_name || '',
|
name: pkg.module_name || '',
|
||||||
|
|
@ -994,18 +993,7 @@ const Packages = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPackageUniqueKey = (pkg: IBPackageWithRepositoryInfo): string => {
|
const initialExpandedPkgs: IBPackageWithRepositoryInfo[] = [];
|
||||||
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 [expandedPkgs, setExpandedPkgs] = useState(initialExpandedPkgs);
|
||||||
|
|
||||||
const setPkgExpanded = (
|
const setPkgExpanded = (
|
||||||
|
|
@ -1013,13 +1001,12 @@ const Packages = () => {
|
||||||
isExpanding: boolean,
|
isExpanding: boolean,
|
||||||
) =>
|
) =>
|
||||||
setExpandedPkgs((prevExpanded) => {
|
setExpandedPkgs((prevExpanded) => {
|
||||||
const pkgKey = getPackageUniqueKey(pkg);
|
const otherExpandedPkgs = prevExpanded.filter((p) => p.name !== pkg.name);
|
||||||
const otherExpandedPkgs = prevExpanded.filter((key) => key !== pkgKey);
|
return isExpanding ? [...otherExpandedPkgs, pkg] : otherExpandedPkgs;
|
||||||
return isExpanding ? [...otherExpandedPkgs, pkgKey] : otherExpandedPkgs;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
||||||
expandedPkgs.includes(getPackageUniqueKey(pkg));
|
expandedPkgs.includes(pkg);
|
||||||
|
|
||||||
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
||||||
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
||||||
|
|
@ -1043,37 +1030,51 @@ const Packages = () => {
|
||||||
'asc' | 'desc'
|
'asc' | 'desc'
|
||||||
>('asc');
|
>('asc');
|
||||||
|
|
||||||
const sortedPackages = useMemo(() => {
|
const getSortableRowValues = (
|
||||||
if (!transformedPackages || !Array.isArray(transformedPackages)) {
|
pkg: IBPackageWithRepositoryInfo,
|
||||||
return [];
|
): (string | number | ApiPackageSourcesResponse[] | undefined)[] => {
|
||||||
}
|
return [pkg.name, pkg.summary, pkg.stream, pkg.end_date, pkg.repository];
|
||||||
|
};
|
||||||
|
|
||||||
return orderBy(
|
let sortedPackages = transformedPackages;
|
||||||
transformedPackages,
|
sortedPackages = transformedPackages.sort((a, b) => {
|
||||||
[
|
const aValue = getSortableRowValues(a)[activeSortIndex];
|
||||||
// Active stream packages first (if activeStream is set)
|
const bValue = getSortableRowValues(b)[activeSortIndex];
|
||||||
(pkg) => (activeStream && pkg.stream === activeStream ? 0 : 1),
|
if (typeof aValue === 'number') {
|
||||||
// Then by name
|
// Numeric sort
|
||||||
'name',
|
if (activeSortDirection === 'asc') {
|
||||||
// Then by stream version (descending)
|
return (aValue as number) - (bValue as number);
|
||||||
(pkg) => {
|
}
|
||||||
if (!pkg.stream) return '';
|
return (bValue as number) - (aValue as number);
|
||||||
const parts = pkg.stream
|
}
|
||||||
.split('.')
|
// String sort
|
||||||
.map((part) => parseInt(part, 10) || 0);
|
// if active stream is set, sort it to the top
|
||||||
// Convert to string with zero-padding for proper sorting
|
if (aValue === activeStream) {
|
||||||
return parts.map((p) => p.toString().padStart(10, '0')).join('.');
|
return -1;
|
||||||
},
|
}
|
||||||
// Then by end date (nulls last)
|
if (bValue === activeStream) {
|
||||||
(pkg) => pkg.end_date || '9999-12-31',
|
return 1;
|
||||||
// Then by repository
|
}
|
||||||
(pkg) => pkg.repository || '',
|
if (activeSortDirection === 'asc') {
|
||||||
// Finally by module name
|
// handle packages with undefined stream
|
||||||
(pkg) => pkg.module_name || '',
|
if (!aValue) {
|
||||||
],
|
return -1;
|
||||||
['asc', 'asc', 'desc', 'asc', 'asc', 'asc'],
|
}
|
||||||
);
|
if (!bValue) {
|
||||||
}, [transformedPackages, activeStream]);
|
return 1;
|
||||||
|
}
|
||||||
|
return (aValue as string).localeCompare(bValue as string);
|
||||||
|
} else {
|
||||||
|
// handle packages with undefined stream
|
||||||
|
if (!aValue) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!bValue) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return (bValue as string).localeCompare(aValue as string);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const getSortParams = (columnIndex: number) => ({
|
const getSortParams = (columnIndex: number) => ({
|
||||||
sortBy: {
|
sortBy: {
|
||||||
|
|
@ -1099,14 +1100,14 @@ const Packages = () => {
|
||||||
(module) => module.name === pkg.name,
|
(module) => module.name === pkg.name,
|
||||||
);
|
);
|
||||||
isSelected =
|
isSelected =
|
||||||
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
packages.some((p) => p.name === pkg.name) && !isModuleWithSameName;
|
||||||
!isModuleWithSameName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pkg.type === 'module') {
|
if (pkg.type === 'module') {
|
||||||
// the package is selected if its module stream matches one in enabled_modules
|
// the package is selected if it's added to the packages state
|
||||||
|
// and its module stream matches one in enabled_modules
|
||||||
isSelected =
|
isSelected =
|
||||||
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
packages.some((p) => p.name === pkg.name) &&
|
||||||
modules.some(
|
modules.some(
|
||||||
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
|
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
|
||||||
);
|
);
|
||||||
|
|
@ -1207,7 +1208,7 @@ const Packages = () => {
|
||||||
.slice(computeStart(), computeEnd())
|
.slice(computeStart(), computeEnd())
|
||||||
.map((grp, rowIndex) => (
|
.map((grp, rowIndex) => (
|
||||||
<Tbody
|
<Tbody
|
||||||
key={`${grp.name}-${grp.repository || 'default'}`}
|
key={`${grp.name}-${rowIndex}`}
|
||||||
isExpanded={isGroupExpanded(grp.name)}
|
isExpanded={isGroupExpanded(grp.name)}
|
||||||
>
|
>
|
||||||
<Tr data-testid='package-row'>
|
<Tr data-testid='package-row'>
|
||||||
|
|
@ -1307,7 +1308,7 @@ const Packages = () => {
|
||||||
.slice(computeStart(), computeEnd())
|
.slice(computeStart(), computeEnd())
|
||||||
.map((pkg, rowIndex) => (
|
.map((pkg, rowIndex) => (
|
||||||
<Tbody
|
<Tbody
|
||||||
key={`${pkg.name}-${pkg.stream || 'default'}-${pkg.module_name || pkg.name}`}
|
key={`${pkg.name}-${rowIndex}`}
|
||||||
isExpanded={isPkgExpanded(pkg)}
|
isExpanded={isPkgExpanded(pkg)}
|
||||||
>
|
>
|
||||||
<Tr data-testid='package-row'>
|
<Tr data-testid='package-row'>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopy,
|
ClipboardCopy,
|
||||||
|
|
@ -16,7 +16,6 @@ import ActivationKeysList from './components/ActivationKeysList';
|
||||||
import Registration from './components/Registration';
|
import Registration from './components/Registration';
|
||||||
import SatelliteRegistration from './components/SatelliteRegistration';
|
import SatelliteRegistration from './components/SatelliteRegistration';
|
||||||
|
|
||||||
import { useGetUser } from '../../../../Hooks';
|
|
||||||
import { useAppSelector } from '../../../../store/hooks';
|
import { useAppSelector } from '../../../../store/hooks';
|
||||||
import {
|
import {
|
||||||
selectActivationKey,
|
selectActivationKey,
|
||||||
|
|
@ -25,7 +24,18 @@ import {
|
||||||
|
|
||||||
const RegistrationStep = () => {
|
const RegistrationStep = () => {
|
||||||
const { auth } = useChrome();
|
const { auth } = useChrome();
|
||||||
const { orgId } = useGetUser(auth);
|
const [orgId, setOrgId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const userData = await auth.getUser();
|
||||||
|
const id = userData?.identity?.internal?.org_id;
|
||||||
|
setOrgId(id);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const activationKey = useAppSelector(selectActivationKey);
|
const activationKey = useAppSelector(selectActivationKey);
|
||||||
const registrationType = useAppSelector(selectRegistrationType);
|
const registrationType = useAppSelector(selectRegistrationType);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -14,12 +14,12 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
|
|
||||||
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
|
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
|
||||||
import {
|
import {
|
||||||
useComposeBPWithNotification as useComposeBlueprintMutation,
|
useComposeBPWithNotification as useComposeBlueprintMutation,
|
||||||
useCreateBPWithNotification as useCreateBlueprintMutation,
|
useCreateBPWithNotification as useCreateBlueprintMutation,
|
||||||
useGetUser,
|
|
||||||
} from '../../../../../Hooks';
|
} from '../../../../../Hooks';
|
||||||
import { setBlueprintId } from '../../../../../store/BlueprintSlice';
|
import { setBlueprintId } from '../../../../../store/BlueprintSlice';
|
||||||
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
|
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
|
||||||
|
|
@ -44,8 +44,19 @@ export const CreateSaveAndBuildBtn = ({
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: CreateDropdownProps) => {
|
}: CreateDropdownProps) => {
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const packages = useAppSelector(selectPackages);
|
const packages = useAppSelector(selectPackages);
|
||||||
|
|
||||||
|
|
@ -102,7 +113,17 @@ export const CreateSaveButton = ({
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: CreateDropdownProps) => {
|
}: CreateDropdownProps) => {
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const packages = useAppSelector(selectPackages);
|
const packages = useAppSelector(selectPackages);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
|
|
@ -9,11 +9,11 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
|
|
||||||
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
|
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
|
||||||
import {
|
import {
|
||||||
useComposeBPWithNotification as useComposeBlueprintMutation,
|
useComposeBPWithNotification as useComposeBlueprintMutation,
|
||||||
useGetUser,
|
|
||||||
useUpdateBPWithNotification as useUpdateBlueprintMutation,
|
useUpdateBPWithNotification as useUpdateBlueprintMutation,
|
||||||
} from '../../../../../Hooks';
|
} from '../../../../../Hooks';
|
||||||
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
|
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
|
||||||
|
|
@ -37,8 +37,19 @@ export const EditSaveAndBuildBtn = ({
|
||||||
blueprintId,
|
blueprintId,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: EditDropdownProps) => {
|
}: EditDropdownProps) => {
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { trigger: buildBlueprint } = useComposeBlueprintMutation();
|
const { trigger: buildBlueprint } = useComposeBlueprintMutation();
|
||||||
const packages = useAppSelector(selectPackages);
|
const packages = useAppSelector(selectPackages);
|
||||||
|
|
@ -94,8 +105,19 @@ export const EditSaveButton = ({
|
||||||
blueprintId,
|
blueprintId,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: EditDropdownProps) => {
|
}: EditDropdownProps) => {
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const packages = useAppSelector(selectPackages);
|
const packages = useAppSelector(selectPackages);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import { EditSaveAndBuildBtn, EditSaveButton } from './EditDropdown';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useCreateBPWithNotification as useCreateBlueprintMutation,
|
useCreateBPWithNotification as useCreateBlueprintMutation,
|
||||||
useGetUser,
|
|
||||||
useUpdateBPWithNotification as useUpdateBlueprintMutation,
|
useUpdateBPWithNotification as useUpdateBlueprintMutation,
|
||||||
} from '../../../../../Hooks';
|
} from '../../../../../Hooks';
|
||||||
import { resolveRelPath } from '../../../../../Utilities/path';
|
import { resolveRelPath } from '../../../../../Utilities/path';
|
||||||
|
|
@ -34,7 +33,6 @@ const ReviewWizardFooter = () => {
|
||||||
const { isSuccess: isUpdateSuccess, reset: resetUpdate } =
|
const { isSuccess: isUpdateSuccess, reset: resetUpdate } =
|
||||||
useUpdateBlueprintMutation({ fixedCacheKey: 'updateBlueprintKey' });
|
useUpdateBlueprintMutation({ fixedCacheKey: 'updateBlueprintKey' });
|
||||||
const { auth } = useChrome();
|
const { auth } = useChrome();
|
||||||
const { orgId } = useGetUser(auth);
|
|
||||||
const { composeId } = useParams();
|
const { composeId } = useParams();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
@ -54,12 +52,14 @@ const ReviewWizardFooter = () => {
|
||||||
|
|
||||||
const getBlueprintPayload = async () => {
|
const getBlueprintPayload = async () => {
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
if (!process.env.IS_ON_PREMISE) {
|
||||||
|
const userData = await auth.getUser();
|
||||||
|
const orgId = userData?.identity?.internal?.org_id;
|
||||||
const requestBody = orgId && mapRequestFromState(store, orgId);
|
const requestBody = orgId && mapRequestFromState(store, orgId);
|
||||||
return requestBody;
|
return requestBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: This is fine for on prem because we save the org id
|
// NOTE: This should be fine on-prem, we should
|
||||||
// to state through a form field in the registration step
|
// be able to ignore the `org-id`
|
||||||
return mapRequestFromState(store, '');
|
return mapRequestFromState(store, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import {
|
||||||
KernelList,
|
KernelList,
|
||||||
LocaleList,
|
LocaleList,
|
||||||
OscapList,
|
OscapList,
|
||||||
RegisterAapList,
|
|
||||||
RegisterLaterList,
|
RegisterLaterList,
|
||||||
RegisterNowList,
|
RegisterNowList,
|
||||||
RegisterSatelliteList,
|
RegisterSatelliteList,
|
||||||
|
|
@ -43,7 +42,6 @@ import isRhel from '../../../../../src/Utilities/isRhel';
|
||||||
import { targetOptions } from '../../../../constants';
|
import { targetOptions } from '../../../../constants';
|
||||||
import { useAppSelector } from '../../../../store/hooks';
|
import { useAppSelector } from '../../../../store/hooks';
|
||||||
import {
|
import {
|
||||||
selectAapRegistration,
|
|
||||||
selectBlueprintDescription,
|
selectBlueprintDescription,
|
||||||
selectBlueprintName,
|
selectBlueprintName,
|
||||||
selectCompliancePolicyID,
|
selectCompliancePolicyID,
|
||||||
|
|
@ -67,7 +65,6 @@ import { useHasSpecificTargetOnly } from '../../utilities/hasSpecificTargetOnly'
|
||||||
const Review = () => {
|
const Review = () => {
|
||||||
const { goToStepById } = useWizardContext();
|
const { goToStepById } = useWizardContext();
|
||||||
|
|
||||||
const aapRegistration = useAppSelector(selectAapRegistration);
|
|
||||||
const blueprintName = useAppSelector(selectBlueprintName);
|
const blueprintName = useAppSelector(selectBlueprintName);
|
||||||
const blueprintDescription = useAppSelector(selectBlueprintDescription);
|
const blueprintDescription = useAppSelector(selectBlueprintDescription);
|
||||||
const distribution = useAppSelector(selectDistribution);
|
const distribution = useAppSelector(selectDistribution);
|
||||||
|
|
@ -86,7 +83,6 @@ const Review = () => {
|
||||||
const users = useAppSelector(selectUsers);
|
const users = useAppSelector(selectUsers);
|
||||||
const kernel = useAppSelector(selectKernel);
|
const kernel = useAppSelector(selectKernel);
|
||||||
|
|
||||||
const [isExpandedAap, setIsExpandedAap] = useState(true);
|
|
||||||
const [isExpandedImageOutput, setIsExpandedImageOutput] = useState(true);
|
const [isExpandedImageOutput, setIsExpandedImageOutput] = useState(true);
|
||||||
const [isExpandedTargetEnvs, setIsExpandedTargetEnvs] = useState(true);
|
const [isExpandedTargetEnvs, setIsExpandedTargetEnvs] = useState(true);
|
||||||
const [isExpandedFSC, setIsExpandedFSC] = useState(true);
|
const [isExpandedFSC, setIsExpandedFSC] = useState(true);
|
||||||
|
|
@ -105,8 +101,6 @@ const Review = () => {
|
||||||
const [isExpandableFirstBoot, setIsExpandedFirstBoot] = useState(true);
|
const [isExpandableFirstBoot, setIsExpandedFirstBoot] = useState(true);
|
||||||
const [isExpandedUsers, setIsExpandedUsers] = useState(true);
|
const [isExpandedUsers, setIsExpandedUsers] = useState(true);
|
||||||
|
|
||||||
const onToggleAap = (isExpandedAap: boolean) =>
|
|
||||||
setIsExpandedAap(isExpandedAap);
|
|
||||||
const onToggleImageOutput = (isExpandedImageOutput: boolean) =>
|
const onToggleImageOutput = (isExpandedImageOutput: boolean) =>
|
||||||
setIsExpandedImageOutput(isExpandedImageOutput);
|
setIsExpandedImageOutput(isExpandedImageOutput);
|
||||||
const onToggleTargetEnvs = (isExpandedTargetEnvs: boolean) =>
|
const onToggleTargetEnvs = (isExpandedTargetEnvs: boolean) =>
|
||||||
|
|
@ -505,21 +499,6 @@ const Review = () => {
|
||||||
<ServicesList />
|
<ServicesList />
|
||||||
</ExpandableSection>
|
</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 && (
|
{!process.env.IS_ON_PREMISE && (
|
||||||
<ExpandableSection
|
<ExpandableSection
|
||||||
toggleContent={composeExpandable(
|
toggleContent={composeExpandable(
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,6 @@ import { useAppSelector } from '../../../../store/hooks';
|
||||||
import { useGetSourceListQuery } from '../../../../store/provisioningApi';
|
import { useGetSourceListQuery } from '../../../../store/provisioningApi';
|
||||||
import { useShowActivationKeyQuery } from '../../../../store/rhsmApi';
|
import { useShowActivationKeyQuery } from '../../../../store/rhsmApi';
|
||||||
import {
|
import {
|
||||||
selectAapCallbackUrl,
|
|
||||||
selectAapHostConfigKey,
|
|
||||||
selectAapTlsCertificateAuthority,
|
|
||||||
selectAapTlsConfirmation,
|
|
||||||
selectActivationKey,
|
selectActivationKey,
|
||||||
selectArchitecture,
|
selectArchitecture,
|
||||||
selectAwsAccountId,
|
selectAwsAccountId,
|
||||||
|
|
@ -664,45 +660,6 @@ export const RegisterSatelliteList = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RegisterAapList = () => {
|
|
||||||
const callbackUrl = useAppSelector(selectAapCallbackUrl);
|
|
||||||
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
|
|
||||||
const tlsCertificateAuthority = useAppSelector(
|
|
||||||
selectAapTlsCertificateAuthority,
|
|
||||||
);
|
|
||||||
const skipTlsVerification = useAppSelector(selectAapTlsConfirmation);
|
|
||||||
|
|
||||||
const getTlsStatus = () => {
|
|
||||||
if (skipTlsVerification) {
|
|
||||||
return 'Insecure (TLS verification skipped)';
|
|
||||||
}
|
|
||||||
return tlsCertificateAuthority ? 'Configured' : 'None';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Content>
|
|
||||||
<Content component={ContentVariants.dl} className='review-step-dl'>
|
|
||||||
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
|
|
||||||
Ansible Callback URL
|
|
||||||
</Content>
|
|
||||||
<Content component={ContentVariants.dd}>
|
|
||||||
{callbackUrl || 'None'}
|
|
||||||
</Content>
|
|
||||||
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
|
|
||||||
Host Config Key
|
|
||||||
</Content>
|
|
||||||
<Content component={ContentVariants.dd}>
|
|
||||||
{hostConfigKey || 'None'}
|
|
||||||
</Content>
|
|
||||||
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
|
|
||||||
TLS Certificate
|
|
||||||
</Content>
|
|
||||||
<Content component={ContentVariants.dd}>{getTlsStatus()}</Content>
|
|
||||||
</Content>
|
|
||||||
</Content>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RegisterNowList = () => {
|
export const RegisterNowList = () => {
|
||||||
const activationKey = useAppSelector(selectActivationKey);
|
const activationKey = useAppSelector(selectActivationKey);
|
||||||
const registrationType = useAppSelector(selectRegistrationType);
|
const registrationType = useAppSelector(selectRegistrationType);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import {
|
||||||
CockpitUploadTypes,
|
CockpitUploadTypes,
|
||||||
} from '../../../store/cockpit/types';
|
} from '../../../store/cockpit/types';
|
||||||
import {
|
import {
|
||||||
AapRegistration,
|
|
||||||
AwsUploadRequestOptions,
|
AwsUploadRequestOptions,
|
||||||
AzureUploadRequestOptions,
|
AzureUploadRequestOptions,
|
||||||
BlueprintExportResponse,
|
BlueprintExportResponse,
|
||||||
|
|
@ -50,11 +49,6 @@ import { ApiRepositoryImportResponseRead } from '../../../store/service/contentS
|
||||||
import {
|
import {
|
||||||
ComplianceType,
|
ComplianceType,
|
||||||
initialState,
|
initialState,
|
||||||
RegistrationType,
|
|
||||||
selectAapCallbackUrl,
|
|
||||||
selectAapHostConfigKey,
|
|
||||||
selectAapTlsCertificateAuthority,
|
|
||||||
selectAapTlsConfirmation,
|
|
||||||
selectActivationKey,
|
selectActivationKey,
|
||||||
selectArchitecture,
|
selectArchitecture,
|
||||||
selectAwsAccountId,
|
selectAwsAccountId,
|
||||||
|
|
@ -211,9 +205,8 @@ function commonRequestToState(
|
||||||
snapshot_date = '';
|
snapshot_date = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to check for the region for on-prem
|
|
||||||
const awsUploadOptions = aws?.upload_request
|
const awsUploadOptions = aws?.upload_request
|
||||||
.options as AwsUploadRequestOptions & { region?: string | undefined };
|
.options as AwsUploadRequestOptions;
|
||||||
const gcpUploadOptions = gcp?.upload_request
|
const gcpUploadOptions = gcp?.upload_request
|
||||||
.options as GcpUploadRequestOptions;
|
.options as GcpUploadRequestOptions;
|
||||||
const azureUploadOptions = azure?.upload_request
|
const azureUploadOptions = azure?.upload_request
|
||||||
|
|
@ -316,7 +309,6 @@ function commonRequestToState(
|
||||||
: 'manual') as AwsShareMethod,
|
: 'manual') as AwsShareMethod,
|
||||||
source: { id: awsUploadOptions?.share_with_sources?.[0] },
|
source: { id: awsUploadOptions?.share_with_sources?.[0] },
|
||||||
sourceId: awsUploadOptions?.share_with_sources?.[0],
|
sourceId: awsUploadOptions?.share_with_sources?.[0],
|
||||||
region: awsUploadOptions?.region,
|
|
||||||
},
|
},
|
||||||
snapshotting: {
|
snapshotting: {
|
||||||
useLatest: !snapshot_date && !request.image_requests[0]?.content_template,
|
useLatest: !snapshot_date && !request.image_requests[0]?.content_template,
|
||||||
|
|
@ -395,7 +387,14 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
||||||
baseUrl: request.customizations.subscription?.['base-url'] || '',
|
baseUrl: request.customizations.subscription?.['base-url'] || '',
|
||||||
},
|
},
|
||||||
registration: {
|
registration: {
|
||||||
registrationType: getRegistrationType(request),
|
registrationType:
|
||||||
|
request.customizations?.subscription && isRhel(request.distribution)
|
||||||
|
? request.customizations.subscription.rhc
|
||||||
|
? 'register-now-rhc'
|
||||||
|
: 'register-now-insights'
|
||||||
|
: getSatelliteCommand(request.customizations.files)
|
||||||
|
? 'register-satellite'
|
||||||
|
: 'register-later',
|
||||||
activationKey: isRhel(request.distribution)
|
activationKey: isRhel(request.distribution)
|
||||||
? request.customizations.subscription?.['activation-key']
|
? request.customizations.subscription?.['activation-key']
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
@ -404,15 +403,6 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
||||||
caCert: request.customizations.cacerts?.pem_certs[0],
|
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),
|
...commonRequestToState(request),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -462,15 +452,6 @@ export const mapExportRequestToState = (
|
||||||
},
|
},
|
||||||
env: initialState.env,
|
env: initialState.env,
|
||||||
registration: initialState.registration,
|
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),
|
...commonRequestToState(blueprintResponse),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -480,24 +461,6 @@ const getFirstBootScript = (files?: File[]): string => {
|
||||||
return firstBootFile?.data ? atob(firstBootFile.data) : '';
|
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 = (
|
const getImageRequests = (
|
||||||
state: RootState,
|
state: RootState,
|
||||||
): ImageRequest[] | CockpitImageRequest[] => {
|
): ImageRequest[] | CockpitImageRequest[] => {
|
||||||
|
|
@ -519,24 +482,6 @@ const getImageRequests = (
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRegistrationType = (request: BlueprintResponse): RegistrationType => {
|
|
||||||
const subscription = request.customizations.subscription;
|
|
||||||
const distribution = request.distribution;
|
|
||||||
const files = request.customizations.files;
|
|
||||||
|
|
||||||
if (subscription && isRhel(distribution)) {
|
|
||||||
if (subscription.rhc) {
|
|
||||||
return 'register-now-rhc';
|
|
||||||
} else {
|
|
||||||
return 'register-now-insights';
|
|
||||||
}
|
|
||||||
} else if (getSatelliteCommand(files)) {
|
|
||||||
return 'register-satellite';
|
|
||||||
} else {
|
|
||||||
return 'register-later';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSatelliteCommand = (files?: File[]): string => {
|
const getSatelliteCommand = (files?: File[]): string => {
|
||||||
const satelliteCommandFile = files?.find(
|
const satelliteCommandFile = files?.find(
|
||||||
(file) => file.path === SATELLITE_PATH,
|
(file) => file.path === SATELLITE_PATH,
|
||||||
|
|
@ -697,7 +642,6 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
|
||||||
pem_certs: [satCert],
|
pem_certs: [satCert],
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
aap_registration: getAapRegistration(state),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ import { useAppSelector } from '../../../store/hooks';
|
||||||
import { BlueprintsResponse } from '../../../store/imageBuilderApi';
|
import { BlueprintsResponse } from '../../../store/imageBuilderApi';
|
||||||
import { useShowActivationKeyQuery } from '../../../store/rhsmApi';
|
import { useShowActivationKeyQuery } from '../../../store/rhsmApi';
|
||||||
import {
|
import {
|
||||||
selectAapCallbackUrl,
|
|
||||||
selectAapHostConfigKey,
|
|
||||||
selectAapTlsCertificateAuthority,
|
|
||||||
selectAapTlsConfirmation,
|
|
||||||
selectActivationKey,
|
selectActivationKey,
|
||||||
selectBlueprintDescription,
|
selectBlueprintDescription,
|
||||||
selectBlueprintId,
|
selectBlueprintId,
|
||||||
|
|
@ -58,8 +54,6 @@ import {
|
||||||
isSshKeyValid,
|
isSshKeyValid,
|
||||||
isUserGroupValid,
|
isUserGroupValid,
|
||||||
isUserNameValid,
|
isUserNameValid,
|
||||||
isValidUrl,
|
|
||||||
validateMultipleCertificates,
|
|
||||||
} from '../validators';
|
} from '../validators';
|
||||||
|
|
||||||
export type StepValidation = {
|
export type StepValidation = {
|
||||||
|
|
@ -211,62 +205,6 @@ export function useRegistrationValidation(): StepValidation {
|
||||||
return { errors: {}, disabledNext: false };
|
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 {
|
export function useFilesystemValidation(): StepValidation {
|
||||||
const mode = useAppSelector(selectFileSystemConfigurationType);
|
const mode = useAppSelector(selectFileSystemConfigurationType);
|
||||||
const partitions = useAppSelector(selectPartitions);
|
const partitions = useAppSelector(selectPartitions);
|
||||||
|
|
|
||||||
|
|
@ -138,85 +138,3 @@ export const isServiceValid = (service: string) => {
|
||||||
/[a-zA-Z]+/.test(service) // contains at least one letter
|
/[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 from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
|
@ -13,11 +13,11 @@ import {
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
|
|
||||||
import ClonesTable from './ClonesTable';
|
import ClonesTable from './ClonesTable';
|
||||||
|
|
||||||
import { AMPLITUDE_MODULE_NAME } from '../../constants';
|
import { AMPLITUDE_MODULE_NAME } from '../../constants';
|
||||||
import { useGetUser } from '../../Hooks';
|
|
||||||
import { useGetComposeStatusQuery } from '../../store/backendApi';
|
import { useGetComposeStatusQuery } from '../../store/backendApi';
|
||||||
import { extractProvisioningList } from '../../store/helpers';
|
import { extractProvisioningList } from '../../store/helpers';
|
||||||
import {
|
import {
|
||||||
|
|
@ -119,7 +119,7 @@ const AwsSourceName = ({ id }: AwsSourceNamePropTypes) => {
|
||||||
return <SourceNotFoundPopover />;
|
return <SourceNotFoundPopover />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseGcpSharedWith = (
|
const parseGcpSharedWith = (
|
||||||
sharedWith: GcpUploadRequestOptions['share_with_accounts'],
|
sharedWith: GcpUploadRequestOptions['share_with_accounts'],
|
||||||
) => {
|
) => {
|
||||||
if (sharedWith) {
|
if (sharedWith) {
|
||||||
|
|
@ -134,9 +134,19 @@ type AwsDetailsPropTypes = {
|
||||||
|
|
||||||
export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => {
|
export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => {
|
||||||
const options = compose.request.image_requests[0].upload_request.options;
|
const options = compose.request.image_requests[0].upload_request.options;
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!isAwsUploadRequestOptions(options)) {
|
if (!isAwsUploadRequestOptions(options)) {
|
||||||
throw TypeError(
|
throw TypeError(
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
Tr,
|
Tr,
|
||||||
} from '@patternfly/react-table';
|
} from '@patternfly/react-table';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { useFlag } from '@unleash/proxy-client-react';
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { NavigateFunction, useNavigate } from 'react-router-dom';
|
import { NavigateFunction, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
@ -58,7 +58,6 @@ import {
|
||||||
SEARCH_INPUT,
|
SEARCH_INPUT,
|
||||||
STATUS_POLLING_INTERVAL,
|
STATUS_POLLING_INTERVAL,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useGetUser } from '../../Hooks';
|
|
||||||
import {
|
import {
|
||||||
useGetBlueprintComposesQuery,
|
useGetBlueprintComposesQuery,
|
||||||
useGetBlueprintsQuery,
|
useGetBlueprintsQuery,
|
||||||
|
|
@ -88,12 +87,11 @@ import {
|
||||||
timestampToDisplayString,
|
timestampToDisplayString,
|
||||||
timestampToDisplayStringDetailed,
|
timestampToDisplayStringDetailed,
|
||||||
} from '../../Utilities/time';
|
} from '../../Utilities/time';
|
||||||
import { AzureLaunchModal } from '../Launch/AzureLaunchModal';
|
|
||||||
import { OciLaunchModal } from '../Launch/OciLaunchModal';
|
|
||||||
|
|
||||||
const ImagesTable = () => {
|
const ImagesTable = () => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(10);
|
const [perPage, setPerPage] = useState(10);
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||||
const blueprintSearchInput =
|
const blueprintSearchInput =
|
||||||
|
|
@ -106,7 +104,16 @@ const ImagesTable = () => {
|
||||||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||||
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const searchParamsGetBlueprints: GetBlueprintsApiArg = {
|
const searchParamsGetBlueprints: GetBlueprintsApiArg = {
|
||||||
limit: blueprintsLimit,
|
limit: blueprintsLimit,
|
||||||
|
|
@ -375,14 +382,8 @@ type AzureRowPropTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AzureRow = ({ compose, rowIndex }: AzureRowPropTypes) => {
|
const AzureRow = ({ compose, rowIndex }: AzureRowPropTypes) => {
|
||||||
const launchEofFlag = useFlag('image-builder.launcheof');
|
|
||||||
|
|
||||||
const details = <AzureDetails compose={compose} />;
|
const details = <AzureDetails compose={compose} />;
|
||||||
const instance = launchEofFlag ? (
|
const instance = <CloudInstance compose={compose} />;
|
||||||
<AzureLaunchModal compose={compose} />
|
|
||||||
) : (
|
|
||||||
<CloudInstance compose={compose} />
|
|
||||||
);
|
|
||||||
const status = <CloudStatus compose={compose} />;
|
const status = <CloudStatus compose={compose} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -402,18 +403,13 @@ type OciRowPropTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const OciRow = ({ compose, rowIndex }: OciRowPropTypes) => {
|
const OciRow = ({ compose, rowIndex }: OciRowPropTypes) => {
|
||||||
const launchEofFlag = useFlag('image-builder.launcheof');
|
|
||||||
const daysToExpiration = Math.floor(
|
const daysToExpiration = Math.floor(
|
||||||
computeHoursToExpiration(compose.created_at) / 24,
|
computeHoursToExpiration(compose.created_at) / 24,
|
||||||
);
|
);
|
||||||
const isExpired = daysToExpiration >= OCI_STORAGE_EXPIRATION_TIME_IN_DAYS;
|
const isExpired = daysToExpiration >= OCI_STORAGE_EXPIRATION_TIME_IN_DAYS;
|
||||||
|
|
||||||
const details = <OciDetails compose={compose} />;
|
const details = <OciDetails compose={compose} />;
|
||||||
const instance = launchEofFlag ? (
|
const instance = <OciInstance compose={compose} isExpired={isExpired} />;
|
||||||
<OciLaunchModal compose={compose} isExpired={isExpired} />
|
|
||||||
) : (
|
|
||||||
<OciInstance compose={compose} isExpired={isExpired} />
|
|
||||||
);
|
|
||||||
const status = (
|
const status = (
|
||||||
<ExpiringStatus
|
<ExpiringStatus
|
||||||
compose={compose}
|
compose={compose}
|
||||||
|
|
@ -471,8 +467,18 @@ type AwsRowPropTypes = {
|
||||||
|
|
||||||
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const target = <AwsTarget compose={compose} />;
|
const target = <AwsTarget compose={compose} />;
|
||||||
const status = <CloudStatus compose={compose} />;
|
const status = <CloudStatus compose={compose} />;
|
||||||
|
|
@ -547,8 +553,18 @@ const Row = ({
|
||||||
details,
|
details,
|
||||||
instance,
|
instance,
|
||||||
}: RowPropTypes) => {
|
}: RowPropTypes) => {
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const handleToggle = () => setIsExpanded(!isExpanded);
|
const handleToggle = () => setIsExpanded(!isExpanded);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Suspense, useState } from 'react';
|
import React, { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from '@patternfly/react-core/dist/esm/components/List/List';
|
} from '@patternfly/react-core/dist/esm/components/List/List';
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
import { useLoadModule, useScalprum } from '@scalprum/react-core';
|
import { useLoadModule, useScalprum } from '@scalprum/react-core';
|
||||||
import cockpit from 'cockpit';
|
import cockpit from 'cockpit';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
@ -30,7 +31,6 @@ import {
|
||||||
MODAL_ANCHOR,
|
MODAL_ANCHOR,
|
||||||
SEARCH_INPUT,
|
SEARCH_INPUT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useGetUser } from '../../Hooks';
|
|
||||||
import {
|
import {
|
||||||
useGetBlueprintsQuery,
|
useGetBlueprintsQuery,
|
||||||
useGetComposeStatusQuery,
|
useGetComposeStatusQuery,
|
||||||
|
|
@ -54,10 +54,7 @@ import {
|
||||||
isOciUploadStatus,
|
isOciUploadStatus,
|
||||||
} from '../../store/typeGuards';
|
} from '../../store/typeGuards';
|
||||||
import { resolveRelPath } from '../../Utilities/path';
|
import { resolveRelPath } from '../../Utilities/path';
|
||||||
import { useFlag } from '../../Utilities/useGetEnvironment';
|
|
||||||
import useProvisioningPermissions from '../../Utilities/useProvisioningPermissions';
|
import useProvisioningPermissions from '../../Utilities/useProvisioningPermissions';
|
||||||
import { AWSLaunchModal } from '../Launch/AWSLaunchModal';
|
|
||||||
import { GcpLaunchModal } from '../Launch/GcpLaunchModal';
|
|
||||||
|
|
||||||
type CloudInstancePropTypes = {
|
type CloudInstancePropTypes = {
|
||||||
compose: ComposesResponseItem;
|
compose: ComposesResponseItem;
|
||||||
|
|
@ -100,12 +97,21 @@ const ProvisioningLink = ({
|
||||||
compose,
|
compose,
|
||||||
composeStatus,
|
composeStatus,
|
||||||
}: ProvisioningLinkPropTypes) => {
|
}: ProvisioningLinkPropTypes) => {
|
||||||
const launchEofFlag = useFlag('image-builder.launcheof');
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [wizardOpen, setWizardOpen] = useState(false);
|
||||||
const [exposedScalprumModule, error] = useLoadModule(
|
const [exposedScalprumModule, error] = useLoadModule(
|
||||||
{
|
{
|
||||||
scope: 'provisioning',
|
scope: 'provisioning',
|
||||||
|
|
@ -176,7 +182,7 @@ const ProvisioningLink = ({
|
||||||
account_id: userData?.identity.internal?.account_id || 'Not found',
|
account_id: userData?.identity.internal?.account_id || 'Not found',
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsModalOpen(true);
|
setWizardOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Launch
|
Launch
|
||||||
|
|
@ -196,10 +202,6 @@ const ProvisioningLink = ({
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
|
|
||||||
setIsModalOpen(!isModalOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Suspense fallback='loading...'>
|
<Suspense fallback='loading...'>
|
||||||
|
|
@ -207,23 +209,7 @@ const ProvisioningLink = ({
|
||||||
compose.blueprint_version !== selectedBlueprintVersion
|
compose.blueprint_version !== selectedBlueprintVersion
|
||||||
? buttonWithTooltip
|
? buttonWithTooltip
|
||||||
: btn}
|
: btn}
|
||||||
{launchEofFlag && isModalOpen && provider === 'aws' && (
|
{wizardOpen && (
|
||||||
<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
|
<Modal
|
||||||
isOpen
|
isOpen
|
||||||
appendTo={appendTo}
|
appendTo={appendTo}
|
||||||
|
|
@ -232,7 +218,7 @@ const ProvisioningLink = ({
|
||||||
>
|
>
|
||||||
<ProvisioningWizard
|
<ProvisioningWizard
|
||||||
hasAccess={permissions[provider]}
|
hasAccess={permissions[provider]}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setWizardOpen(false)}
|
||||||
image={{
|
image={{
|
||||||
name: compose.image_name || compose.id,
|
name: compose.image_name || compose.id,
|
||||||
id: compose.id,
|
id: compose.id,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import './ImageBuildStatus.scss';
|
import './ImageBuildStatus.scss';
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,13 +24,13 @@ import {
|
||||||
PendingIcon,
|
PendingIcon,
|
||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AMPLITUDE_MODULE_NAME,
|
AMPLITUDE_MODULE_NAME,
|
||||||
AWS_S3_EXPIRATION_TIME_IN_HOURS,
|
AWS_S3_EXPIRATION_TIME_IN_HOURS,
|
||||||
OCI_STORAGE_EXPIRATION_TIME_IN_DAYS,
|
OCI_STORAGE_EXPIRATION_TIME_IN_DAYS,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useGetUser } from '../../Hooks';
|
|
||||||
import { useGetComposeStatusQuery } from '../../store/backendApi';
|
import { useGetComposeStatusQuery } from '../../store/backendApi';
|
||||||
import { CockpitComposesResponseItem } from '../../store/cockpit/types';
|
import { CockpitComposesResponseItem } from '../../store/cockpit/types';
|
||||||
import {
|
import {
|
||||||
|
|
@ -122,8 +122,18 @@ export const CloudStatus = ({ compose }: CloudStatusPropTypes) => {
|
||||||
const { data, isSuccess } = useGetComposeStatusQuery({
|
const { data, isSuccess } = useGetComposeStatusQuery({
|
||||||
composeId: compose.id,
|
composeId: compose.id,
|
||||||
});
|
});
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
return <Skeleton />;
|
return <Skeleton />;
|
||||||
|
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
List,
|
|
||||||
ListComponent,
|
|
||||||
ListItem,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalVariant,
|
|
||||||
OrderType,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ComposesResponseItem,
|
|
||||||
ComposeStatus,
|
|
||||||
} from '../../store/imageBuilderApi';
|
|
||||||
import { isAwsUploadRequestOptions } from '../../store/typeGuards';
|
|
||||||
|
|
||||||
type LaunchProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
handleModalToggle: (event: KeyboardEvent | React.MouseEvent) => void;
|
|
||||||
compose: ComposesResponseItem;
|
|
||||||
composeStatus: ComposeStatus | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AWSLaunchModal = ({
|
|
||||||
isOpen,
|
|
||||||
handleModalToggle,
|
|
||||||
compose,
|
|
||||||
composeStatus,
|
|
||||||
}: LaunchProps) => {
|
|
||||||
const options = compose.request.image_requests[0].upload_request.options;
|
|
||||||
|
|
||||||
if (!isAwsUploadRequestOptions(options)) {
|
|
||||||
throw TypeError(
|
|
||||||
`Error: options must be of type AwsUploadRequestOptions, not ${typeof options}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const amiId =
|
|
||||||
composeStatus?.image_status.status === 'success' &&
|
|
||||||
composeStatus.image_status.upload_status?.options &&
|
|
||||||
'ami' in composeStatus.image_status.upload_status.options
|
|
||||||
? composeStatus.image_status.upload_status.options.ami
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={handleModalToggle}
|
|
||||||
variant={ModalVariant.large}
|
|
||||||
aria-label='Open launch guide modal'
|
|
||||||
>
|
|
||||||
<ModalHeader
|
|
||||||
title={'Launch with Amazon Web Services'}
|
|
||||||
labelId='modal-title'
|
|
||||||
description={compose.image_name}
|
|
||||||
/>
|
|
||||||
<ModalBody id='modal-box-body-basic'>
|
|
||||||
<List component={ListComponent.ol} type={OrderType.number}>
|
|
||||||
<ListItem>
|
|
||||||
Navigate to the{' '}
|
|
||||||
<Button
|
|
||||||
component='a'
|
|
||||||
target='_blank'
|
|
||||||
variant='link'
|
|
||||||
icon={<ExternalLinkAltIcon />}
|
|
||||||
iconPosition='right'
|
|
||||||
href={`https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#ImageDetails:imageId=${amiId}`}
|
|
||||||
className='pf-v6-u-pl-0'
|
|
||||||
>
|
|
||||||
Images detail page
|
|
||||||
</Button>{' '}
|
|
||||||
located on your AWS console.
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
Copy the image to make it a permanent copy in your account.
|
|
||||||
<br />
|
|
||||||
Shared with Account{' '}
|
|
||||||
<span className='pf-v6-u-font-weight-bold'>
|
|
||||||
{options.share_with_accounts?.[0]}
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
AMI ID: <span className='pf-v6-u-font-weight-bold'>{amiId}</span>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>Launch image as an instance.</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
Connect to it via SSH using the following username:{' '}
|
|
||||||
<span className='pf-v6-u-font-weight-bold'>ec2-user</span>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
import React, { Fragment, useState } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
List,
|
|
||||||
ListComponent,
|
|
||||||
ListItem,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalVariant,
|
|
||||||
OrderType,
|
|
||||||
Skeleton,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ComposesResponseItem,
|
|
||||||
useGetComposeStatusQuery,
|
|
||||||
} from '../../store/imageBuilderApi';
|
|
||||||
import { isAzureUploadStatus } from '../../store/typeGuards';
|
|
||||||
|
|
||||||
type LaunchProps = {
|
|
||||||
compose: ComposesResponseItem;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AzureLaunchModal = ({ compose }: LaunchProps) => {
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const { data, isSuccess, isFetching } = useGetComposeStatusQuery({
|
|
||||||
composeId: compose.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isSuccess) {
|
|
||||||
return <Skeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = data?.image_status.upload_status?.options;
|
|
||||||
|
|
||||||
if (options && !isAzureUploadStatus(options)) {
|
|
||||||
throw TypeError(
|
|
||||||
`Error: options must be of type AzureUploadStatus, not ${typeof options}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
|
|
||||||
setIsModalOpen(!isModalOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<Button
|
|
||||||
variant='link'
|
|
||||||
isInline
|
|
||||||
isDisabled={data?.image_status.status !== 'success'}
|
|
||||||
onClick={handleModalToggle}
|
|
||||||
>
|
|
||||||
Launch
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={handleModalToggle}
|
|
||||||
variant={ModalVariant.large}
|
|
||||||
aria-label='Open launch guide wizard'
|
|
||||||
>
|
|
||||||
<ModalHeader
|
|
||||||
title={'Launch with Microsoft Azure'}
|
|
||||||
labelId='modal-title'
|
|
||||||
description={compose.image_name}
|
|
||||||
/>
|
|
||||||
<ModalBody id='modal-box-body-basic'>
|
|
||||||
<List component={ListComponent.ol} type={OrderType.number}>
|
|
||||||
<ListItem>
|
|
||||||
Locate{' '}
|
|
||||||
{!isFetching && (
|
|
||||||
<span className='pf-v6-u-font-weight-bold'>
|
|
||||||
{options?.image_name}{' '}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isFetching && <Skeleton />}
|
|
||||||
in the{' '}
|
|
||||||
<Button
|
|
||||||
component='a'
|
|
||||||
target='_blank'
|
|
||||||
variant='link'
|
|
||||||
icon={<ExternalLinkAltIcon />}
|
|
||||||
iconPosition='right'
|
|
||||||
href={`https://portal.azure.com/#view/Microsoft_Azure_ComputeHub/ComputeHubMenuBlade/~/imagesBrowse`}
|
|
||||||
className='pf-v6-u-pl-0'
|
|
||||||
>
|
|
||||||
Azure console
|
|
||||||
</Button>
|
|
||||||
.
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
Create a Virtual Machine (VM) by using the image.
|
|
||||||
<br />
|
|
||||||
Note: Review the{' '}
|
|
||||||
<span className='pf-v6-u-font-weight-bold'>
|
|
||||||
Availability Zone
|
|
||||||
</span>{' '}
|
|
||||||
and the <span className='pf-v6-u-font-weight-bold'>Size</span> to
|
|
||||||
meet your requirements. Adjust these settings as needed.
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ClipboardCopy,
|
|
||||||
ClipboardCopyVariant,
|
|
||||||
List,
|
|
||||||
ListComponent,
|
|
||||||
ListItem,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalVariant,
|
|
||||||
OrderType,
|
|
||||||
TextInput,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
import { generateDefaultName } from './useGenerateDefaultName';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ComposesResponseItem,
|
|
||||||
ComposeStatus,
|
|
||||||
} from '../../store/imageBuilderApi';
|
|
||||||
import {
|
|
||||||
isGcpUploadRequestOptions,
|
|
||||||
isGcpUploadStatus,
|
|
||||||
} from '../../store/typeGuards';
|
|
||||||
import { parseGcpSharedWith } from '../ImagesTable/ImageDetails';
|
|
||||||
|
|
||||||
type LaunchProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
handleModalToggle: (event: KeyboardEvent | React.MouseEvent) => void;
|
|
||||||
compose: ComposesResponseItem;
|
|
||||||
composeStatus: ComposeStatus | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GcpLaunchModal = ({
|
|
||||||
isOpen,
|
|
||||||
handleModalToggle,
|
|
||||||
compose,
|
|
||||||
composeStatus,
|
|
||||||
}: LaunchProps) => {
|
|
||||||
const [customerProjectId, setCustomerProjectId] = useState('');
|
|
||||||
|
|
||||||
const statusOptions = composeStatus?.image_status.upload_status?.options;
|
|
||||||
const composeOptions =
|
|
||||||
compose.request.image_requests[0].upload_request.options;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(statusOptions && !isGcpUploadStatus(statusOptions)) ||
|
|
||||||
!isGcpUploadRequestOptions(composeOptions)
|
|
||||||
) {
|
|
||||||
throw TypeError(
|
|
||||||
`Error: options must be of type GcpUploadRequestOptions, not ${typeof statusOptions}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageName = statusOptions?.image_name;
|
|
||||||
const projectId = statusOptions?.project_id;
|
|
||||||
if (!imageName || !projectId) {
|
|
||||||
throw TypeError(
|
|
||||||
`Error: Image name not found, unable to generate a command to copy ${typeof statusOptions}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const uniqueImageName = generateDefaultName(imageName);
|
|
||||||
const authorizeString =
|
|
||||||
composeOptions.share_with_accounts &&
|
|
||||||
composeOptions.share_with_accounts.length === 1
|
|
||||||
? `Authorize gcloud CLI to the following
|
|
||||||
account: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.`
|
|
||||||
: composeOptions.share_with_accounts
|
|
||||||
? `Authorize gcloud CLI to use one of the following
|
|
||||||
accounts: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.`
|
|
||||||
: 'Authorize gcloud CLI to use the account that the image is shared with.';
|
|
||||||
const installationCommand = `sudo dnf install google-cloud-cli`;
|
|
||||||
const createImage = `gcloud compute images create ${uniqueImageName} --source-image=${imageName} --source-image-project=${projectId} --project=${
|
|
||||||
customerProjectId || '<your_project_id>'
|
|
||||||
}`;
|
|
||||||
const createInstance = `gcloud compute instances create ${uniqueImageName} --image=${uniqueImageName} --project=${
|
|
||||||
customerProjectId || '<your_project_id>'
|
|
||||||
}`;
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={handleModalToggle}
|
|
||||||
variant={ModalVariant.large}
|
|
||||||
aria-label='Open launch guide modal'
|
|
||||||
>
|
|
||||||
<ModalHeader
|
|
||||||
title={'Launch with Google Cloud Platform'}
|
|
||||||
labelId='modal-title'
|
|
||||||
description={compose.image_name}
|
|
||||||
/>
|
|
||||||
<ModalBody id='modal-box-body-basic'>
|
|
||||||
<List component={ListComponent.ol} type={OrderType.number}>
|
|
||||||
<ListItem>
|
|
||||||
Install the gcloud CLI. See the{' '}
|
|
||||||
<Button
|
|
||||||
component='a'
|
|
||||||
target='_blank'
|
|
||||||
variant='link'
|
|
||||||
icon={<ExternalLinkAltIcon />}
|
|
||||||
iconPosition='right'
|
|
||||||
href={`https://cloud.google.com/sdk/docs/install`}
|
|
||||||
className='pf-v6-u-pl-0'
|
|
||||||
>
|
|
||||||
Install gcloud CLI
|
|
||||||
</Button>
|
|
||||||
documentation.
|
|
||||||
<ClipboardCopy isReadOnly hoverTip='Copy' clickTip='Copied'>
|
|
||||||
{installationCommand}
|
|
||||||
</ClipboardCopy>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>{authorizeString}</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
Enter your GCP project ID, and run the command to create the image
|
|
||||||
in your project.
|
|
||||||
<TextInput
|
|
||||||
className='pf-v6-u-mt-sm pf-v6-u-mb-md'
|
|
||||||
value={customerProjectId}
|
|
||||||
type='text'
|
|
||||||
onChange={(_event, value) => setCustomerProjectId(value)}
|
|
||||||
aria-label='Project ID input'
|
|
||||||
placeholder='Project ID'
|
|
||||||
/>
|
|
||||||
<ClipboardCopy
|
|
||||||
isReadOnly
|
|
||||||
hoverTip='Copy'
|
|
||||||
clickTip='Copied'
|
|
||||||
variant={ClipboardCopyVariant.expansion}
|
|
||||||
>
|
|
||||||
{createImage}
|
|
||||||
</ClipboardCopy>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
Create an instance of your image by either accessing the{' '}
|
|
||||||
<Button
|
|
||||||
component='a'
|
|
||||||
target='_blank'
|
|
||||||
variant='link'
|
|
||||||
icon={<ExternalLinkAltIcon />}
|
|
||||||
iconPosition='right'
|
|
||||||
href={`https://console.cloud.google.com/compute/images`}
|
|
||||||
className='pf-v6-u-pl-0'
|
|
||||||
>
|
|
||||||
GCP console
|
|
||||||
</Button>{' '}
|
|
||||||
or by running the following command:
|
|
||||||
<ClipboardCopy
|
|
||||||
isReadOnly
|
|
||||||
hoverTip='Copy'
|
|
||||||
clickTip='Copied'
|
|
||||||
variant={ClipboardCopyVariant.expansion}
|
|
||||||
>
|
|
||||||
{createInstance}
|
|
||||||
</ClipboardCopy>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
import React, { Fragment, useState } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ClipboardCopy,
|
|
||||||
ClipboardCopyVariant,
|
|
||||||
List,
|
|
||||||
ListComponent,
|
|
||||||
ListItem,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalVariant,
|
|
||||||
OrderType,
|
|
||||||
Skeleton,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ComposesResponseItem,
|
|
||||||
useGetComposeStatusQuery,
|
|
||||||
} from '../../store/imageBuilderApi';
|
|
||||||
import { isOciUploadStatus } from '../../store/typeGuards';
|
|
||||||
import { resolveRelPath } from '../../Utilities/path';
|
|
||||||
|
|
||||||
type LaunchProps = {
|
|
||||||
isExpired: boolean;
|
|
||||||
compose: ComposesResponseItem;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OciLaunchModal = ({ isExpired, compose }: LaunchProps) => {
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const { data, isSuccess, isFetching } = useGetComposeStatusQuery({
|
|
||||||
composeId: compose.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
if (!isSuccess) {
|
|
||||||
return <Skeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = data?.image_status.upload_status?.options;
|
|
||||||
|
|
||||||
if (options && !isOciUploadStatus(options)) {
|
|
||||||
throw TypeError(
|
|
||||||
`Error: options must be of type OciUploadStatus, not ${typeof options}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExpired) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
component='a'
|
|
||||||
target='_blank'
|
|
||||||
variant='link'
|
|
||||||
onClick={() => navigate(resolveRelPath(`imagewizard/${compose.id}`))}
|
|
||||||
isInline
|
|
||||||
>
|
|
||||||
Recreate image
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleModalToggle = () => {
|
|
||||||
setIsModalOpen(!isModalOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<Button
|
|
||||||
variant='link'
|
|
||||||
isInline
|
|
||||||
isDisabled={data?.image_status.status !== 'success'}
|
|
||||||
onClick={handleModalToggle}
|
|
||||||
>
|
|
||||||
Image link
|
|
||||||
</Button>
|
|
||||||
<Modal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={handleModalToggle}
|
|
||||||
variant={ModalVariant.large}
|
|
||||||
aria-label='Open launch guide modal'
|
|
||||||
>
|
|
||||||
<ModalHeader
|
|
||||||
title={'Launch with Oracle Cloud Infrastructure'}
|
|
||||||
labelId='modal-title'
|
|
||||||
description={compose.image_name}
|
|
||||||
/>
|
|
||||||
<ModalBody id='modal-box-body-basic'>
|
|
||||||
<List component={ListComponent.ol} type={OrderType.number}>
|
|
||||||
<ListItem>
|
|
||||||
Navigate to the{' '}
|
|
||||||
<Button
|
|
||||||
component='a'
|
|
||||||
target='_blank'
|
|
||||||
variant='link'
|
|
||||||
icon={<ExternalLinkAltIcon />}
|
|
||||||
iconPosition='right'
|
|
||||||
href={`https://cloud.oracle.com/compute/images`}
|
|
||||||
className='pf-v6-u-pl-0'
|
|
||||||
>
|
|
||||||
Oracle Cloud'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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
export const generateDefaultName = (imageName: string) => {
|
|
||||||
const date = new Date();
|
|
||||||
const day = date.getDate().toString().padStart(2, '0');
|
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
||||||
const year = date.getFullYear().toString();
|
|
||||||
const hours = date.getHours().toString().padStart(2, '0');
|
|
||||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
||||||
|
|
||||||
const dateTimeString = `${month}${day}${year}-${hours}${minutes}`;
|
|
||||||
|
|
||||||
// gcloud images are valid in the form of: (?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)
|
|
||||||
let newBlueprintName = imageName
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9-]/g, '-')
|
|
||||||
.replace(/-{2,}/g, '-')
|
|
||||||
.replace(/^-+|-+$/g, '');
|
|
||||||
|
|
||||||
if (!/^[a-z]/.test(newBlueprintName)) {
|
|
||||||
newBlueprintName = 'i' + newBlueprintName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxLength = 63;
|
|
||||||
const uniquePartLength = dateTimeString.length + 1;
|
|
||||||
const baseNameMaxLength = maxLength - uniquePartLength;
|
|
||||||
if (newBlueprintName.length > baseNameMaxLength) {
|
|
||||||
newBlueprintName = newBlueprintName.substring(0, baseNameMaxLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (newBlueprintName.endsWith('-')) {
|
|
||||||
newBlueprintName = newBlueprintName.slice(0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${newBlueprintName}-${dateTimeString}`;
|
|
||||||
};
|
|
||||||
|
|
@ -156,7 +156,7 @@ const RegionsSelect = ({ composeId, handleClose }: RegionsSelectPropTypes) => {
|
||||||
<MenuToggle
|
<MenuToggle
|
||||||
variant='typeahead'
|
variant='typeahead'
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
ref={toggleRef}
|
innerRef={toggleRef}
|
||||||
isExpanded={isOpen}
|
isExpanded={isOpen}
|
||||||
>
|
>
|
||||||
<TextInputGroup isPlain>
|
<TextInputGroup isPlain>
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,3 @@ export { useDeleteBPWithNotification } from './MutationNotifications/useDeleteBP
|
||||||
export { useFixupBPWithNotification } from './MutationNotifications/useFixupBPWithNotification';
|
export { useFixupBPWithNotification } from './MutationNotifications/useFixupBPWithNotification';
|
||||||
export { useComposeBPWithNotification } from './MutationNotifications/useComposeBPWithNotification';
|
export { useComposeBPWithNotification } from './MutationNotifications/useComposeBPWithNotification';
|
||||||
export { useCloneComposeWithNotification } from './MutationNotifications/useCloneComposeWithNotification';
|
export { useCloneComposeWithNotification } from './MutationNotifications/useCloneComposeWithNotification';
|
||||||
export { useGetUser } from './useGetUser';
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
|
||||||
|
|
||||||
export const useGetUser = (auth: { getUser(): Promise<void | ChromeUser> }) => {
|
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
const [orgId, setOrgId] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
const id = data?.identity.internal?.org_id;
|
|
||||||
setUserData(data);
|
|
||||||
setOrgId(id);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { userData, orgId };
|
|
||||||
};
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* - Please do NOT modify this file.
|
* - Please do NOT modify this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PACKAGE_VERSION = '2.10.5'
|
const PACKAGE_VERSION = '2.10.4'
|
||||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||||
const activeClientIds = new Set()
|
const activeClientIds = new Set()
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,6 @@ export const useLazyGetOscapCustomizationsQuery = process.env.IS_ON_PREMISE
|
||||||
? cockpitQueries.useLazyGetOscapCustomizationsQuery
|
? cockpitQueries.useLazyGetOscapCustomizationsQuery
|
||||||
: serviceQueries.useLazyGetOscapCustomizationsQuery;
|
: serviceQueries.useLazyGetOscapCustomizationsQuery;
|
||||||
|
|
||||||
export const useGetComplianceCustomizationsQuery =
|
|
||||||
serviceQueries.useGetOscapCustomizationsForPolicyQuery;
|
|
||||||
|
|
||||||
export const useLazyGetComplianceCustomizationsQuery =
|
|
||||||
serviceQueries.useLazyGetOscapCustomizationsForPolicyQuery;
|
|
||||||
|
|
||||||
export const useComposeBlueprintMutation = process.env.IS_ON_PREMISE
|
export const useComposeBlueprintMutation = process.env.IS_ON_PREMISE
|
||||||
? cockpitQueries.useComposeBlueprintMutation
|
? cockpitQueries.useComposeBlueprintMutation
|
||||||
: serviceQueries.useComposeBlueprintMutation;
|
: serviceQueries.useComposeBlueprintMutation;
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,7 @@ export type RegistrationType =
|
||||||
| 'register-now'
|
| 'register-now'
|
||||||
| 'register-now-insights'
|
| 'register-now-insights'
|
||||||
| 'register-now-rhc'
|
| 'register-now-rhc'
|
||||||
| 'register-satellite'
|
| 'register-satellite';
|
||||||
| 'register-aap';
|
|
||||||
|
|
||||||
export type ComplianceType = 'openscap' | 'compliance';
|
export type ComplianceType = 'openscap' | 'compliance';
|
||||||
|
|
||||||
|
|
@ -90,12 +89,6 @@ export type wizardState = {
|
||||||
architecture: ImageRequest['architecture'];
|
architecture: ImageRequest['architecture'];
|
||||||
distribution: Distributions;
|
distribution: Distributions;
|
||||||
imageTypes: ImageTypes[];
|
imageTypes: ImageTypes[];
|
||||||
aapRegistration: {
|
|
||||||
callbackUrl: string | undefined;
|
|
||||||
hostConfigKey: string | undefined;
|
|
||||||
tlsCertificateAuthority: string | undefined;
|
|
||||||
skipTlsVerification: boolean | undefined;
|
|
||||||
};
|
|
||||||
aws: {
|
aws: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
shareMethod: AwsShareMethod;
|
shareMethod: AwsShareMethod;
|
||||||
|
|
@ -196,12 +189,6 @@ export const initialState: wizardState = {
|
||||||
architecture: X86_64,
|
architecture: X86_64,
|
||||||
distribution: RHEL_10,
|
distribution: RHEL_10,
|
||||||
imageTypes: [],
|
imageTypes: [],
|
||||||
aapRegistration: {
|
|
||||||
callbackUrl: undefined,
|
|
||||||
hostConfigKey: undefined,
|
|
||||||
tlsCertificateAuthority: undefined,
|
|
||||||
skipTlsVerification: undefined,
|
|
||||||
},
|
|
||||||
aws: {
|
aws: {
|
||||||
accountId: '',
|
accountId: '',
|
||||||
shareMethod: 'sources',
|
shareMethod: 'sources',
|
||||||
|
|
@ -389,26 +376,6 @@ export const selectSatelliteCaCertificate = (state: RootState) => {
|
||||||
return state.wizard.registration.satelliteRegistration.caCert;
|
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) => {
|
export const selectComplianceProfileID = (state: RootState) => {
|
||||||
return state.wizard.compliance.profileID;
|
return state.wizard.compliance.profileID;
|
||||||
};
|
};
|
||||||
|
|
@ -660,22 +627,6 @@ export const wizardSlice = createSlice({
|
||||||
changeSatelliteCaCertificate: (state, action: PayloadAction<string>) => {
|
changeSatelliteCaCertificate: (state, action: PayloadAction<string>) => {
|
||||||
state.registration.satelliteRegistration.caCert = action.payload;
|
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: (
|
changeActivationKey: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<ActivationKeys['name']>,
|
action: PayloadAction<ActivationKeys['name']>,
|
||||||
|
|
@ -1279,10 +1230,6 @@ export const {
|
||||||
changeTimezone,
|
changeTimezone,
|
||||||
changeSatelliteRegistrationCommand,
|
changeSatelliteRegistrationCommand,
|
||||||
changeSatelliteCaCertificate,
|
changeSatelliteCaCertificate,
|
||||||
changeAapCallbackUrl,
|
|
||||||
changeAapHostConfigKey,
|
|
||||||
changeAapTlsCertificateAuthority,
|
|
||||||
changeAapTlsConfirmation,
|
|
||||||
addNtpServer,
|
addNtpServer,
|
||||||
removeNtpServer,
|
removeNtpServer,
|
||||||
changeHostname,
|
changeHostname,
|
||||||
|
|
|
||||||
|
|
@ -714,9 +714,6 @@ describe('Import modal', () => {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// AAP
|
|
||||||
await clickNext();
|
|
||||||
|
|
||||||
// Firstboot
|
// Firstboot
|
||||||
await clickNext();
|
await clickNext();
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ vi.mock('@unleash/proxy-client-react', () => ({
|
||||||
switch (flag) {
|
switch (flag) {
|
||||||
case 'image-builder.compliance.enabled':
|
case 'image-builder.compliance.enabled':
|
||||||
return true;
|
return true;
|
||||||
case 'image-builder.aap.enabled':
|
|
||||||
return true;
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -332,30 +332,4 @@ describe('OpenSCAP edit mode', () => {
|
||||||
user.click(selectedBtn);
|
user.click(selectedBtn);
|
||||||
await screen.findByText('neovim');
|
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,123 +513,6 @@ describe('Step Packages', () => {
|
||||||
expect(secondAppStreamRow).toBeDisabled();
|
expect(secondAppStreamRow).toBeDisabled();
|
||||||
expect(secondAppStreamRow).not.toBeChecked();
|
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;
|
router = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clicking Next loads Ansible Automation Platform', async () => {
|
test('clicking Next loads First boot script', async () => {
|
||||||
await renderCreateMode();
|
await renderCreateMode();
|
||||||
await goToServicesStep();
|
await goToServicesStep();
|
||||||
await clickNext();
|
await clickNext();
|
||||||
await screen.findByRole('heading', {
|
await screen.findByRole('heading', {
|
||||||
name: 'Ansible Automation Platform',
|
name: 'First boot configuration',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
14
src/test/fixtures/compliance.ts
vendored
14
src/test/fixtures/compliance.ts
vendored
|
|
@ -36,20 +36,8 @@ export const mockPolicies = {
|
||||||
profile_title: 'DISA STIG with GUI for Red Hat Enterprise Linux 8',
|
profile_title: 'DISA STIG with GUI for Red Hat Enterprise Linux 8',
|
||||||
ref_id: 'xccdf_org.ssgproject.content_profile_stig_gui',
|
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: {
|
meta: {
|
||||||
total: 4,
|
total: 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
16
src/test/fixtures/oscap.ts
vendored
16
src/test/fixtures/oscap.ts
vendored
|
|
@ -124,17 +124,9 @@ export const oscapCustomizationsPolicy = (
|
||||||
): GetOscapCustomizationsApiResponse => {
|
): GetOscapCustomizationsApiResponse => {
|
||||||
const policyData = mockPolicies.data.find((p) => p.id === policy);
|
const policyData = mockPolicies.data.find((p) => p.id === policy);
|
||||||
const customizations = oscapCustomizations(policyData!.ref_id);
|
const customizations = oscapCustomizations(policyData!.ref_id);
|
||||||
|
// filter out a single package to simulate the customizations being tailored
|
||||||
// Simulate different levels of customization based on policy
|
customizations.packages = customizations.packages!.filter(
|
||||||
if (policy === 'custom-policy-123') {
|
(p) => p !== 'aide',
|
||||||
// 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;
|
return customizations;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
58
src/test/fixtures/packages.ts
vendored
58
src/test/fixtures/packages.ts
vendored
|
|
@ -75,64 +75,6 @@ export const mockSourcesPackagesResults = (
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (search === 'sortingTest') {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
package_name: 'alphaModule',
|
|
||||||
summary: 'Alpha module for sorting tests',
|
|
||||||
package_sources: [
|
|
||||||
{
|
|
||||||
name: 'alphaModule',
|
|
||||||
type: 'module',
|
|
||||||
stream: '2.0',
|
|
||||||
end_date: '2025-12-01',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'alphaModule',
|
|
||||||
type: 'module',
|
|
||||||
stream: '3.0',
|
|
||||||
end_date: '2027-12-01',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
package_name: 'betaModule',
|
|
||||||
summary: 'Beta module for sorting tests',
|
|
||||||
package_sources: [
|
|
||||||
{
|
|
||||||
name: 'betaModule',
|
|
||||||
type: 'module',
|
|
||||||
stream: '2.0',
|
|
||||||
end_date: '2025-06-01',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'betaModule',
|
|
||||||
type: 'module',
|
|
||||||
stream: '4.0',
|
|
||||||
end_date: '2028-06-01',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
package_name: 'gammaModule',
|
|
||||||
summary: 'Gamma module for sorting tests',
|
|
||||||
package_sources: [
|
|
||||||
{
|
|
||||||
name: 'gammaModule',
|
|
||||||
type: 'module',
|
|
||||||
stream: '2.0',
|
|
||||||
end_date: '2025-08-01',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'gammaModule',
|
|
||||||
type: 'module',
|
|
||||||
stream: '1.5',
|
|
||||||
end_date: '2026-08-01',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (search === 'mock') {
|
if (search === 'mock') {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,6 @@ vi.mock('@unleash/proxy-client-react', () => ({
|
||||||
return true;
|
return true;
|
||||||
case 'image-builder.templates.enabled':
|
case 'image-builder.templates.enabled':
|
||||||
return true;
|
return true;
|
||||||
case 'image-builder.aap.enabled':
|
|
||||||
return true;
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue