Compare commits
31 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
080513ad6d | ||
|
|
7391652e17 | ||
|
|
9a17373234 | ||
|
|
d7f844b8b6 | ||
|
|
859b7cace8 | ||
|
|
3a83a14720 | ||
|
|
e61cb99f1b | ||
|
|
a5aa15cbcb | ||
|
|
44c3674072 | ||
|
|
4d783537fb | ||
|
|
0b96c64c93 | ||
|
|
0d917c3cd8 | ||
|
|
957700adcc | ||
|
|
fa0560ac4d | ||
|
|
0e7f5d9e7b | ||
|
|
e0dd33fdc9 | ||
|
|
b0393a5f4f | ||
|
|
a9d2ba59a8 | ||
|
|
af19251f17 | ||
|
|
090544c333 | ||
|
|
4b188a0393 | ||
|
|
63f55c7408 | ||
|
|
bc3288a83e | ||
|
|
3e5c5dca76 | ||
|
|
04f0528701 | ||
|
|
3e2e9dcaa6 | ||
|
|
54e413f459 | ||
|
|
f6f6e58449 | ||
|
|
bf77501eea | ||
|
|
42b16bafd8 | ||
|
|
04adcc133c |
57 changed files with 2743 additions and 1801 deletions
257
.forgejo/workflows/ci.yml
Normal file
257
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
---
|
||||||
|
name: Debian Image Builder Frontend CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: "18"
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
name: Build and Test Frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:18-bullseye
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
git \
|
||||||
|
ca-certificates \
|
||||||
|
python3
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build || echo "Build script not found"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
if [ -f package.json ] && npm run test; then
|
||||||
|
npm test
|
||||||
|
else
|
||||||
|
echo "No test script found, skipping tests"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run linting
|
||||||
|
run: |
|
||||||
|
if [ -f package.json ] && npm run lint; then
|
||||||
|
npm run lint
|
||||||
|
else
|
||||||
|
echo "No lint script found, skipping linting"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build production bundle
|
||||||
|
run: |
|
||||||
|
if [ -f package.json ] && npm run build; then
|
||||||
|
npm run build
|
||||||
|
else
|
||||||
|
echo "No build script found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: frontend-build
|
||||||
|
path: |
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: Package Frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:18-bullseye
|
||||||
|
needs: build-and-test
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
devscripts \
|
||||||
|
debhelper \
|
||||||
|
git \
|
||||||
|
ca-certificates \
|
||||||
|
python3
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build production bundle
|
||||||
|
run: |
|
||||||
|
if [ -f package.json ] && npm run build; then
|
||||||
|
npm run build
|
||||||
|
else
|
||||||
|
echo "No build script found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create debian directory
|
||||||
|
run: |
|
||||||
|
mkdir -p debian
|
||||||
|
cat > debian/control << EOF
|
||||||
|
Source: debian-image-builder-frontend
|
||||||
|
Section: web
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Debian Forge Team <team@debian-forge.org>
|
||||||
|
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
||||||
|
Standards-Version: 4.6.2
|
||||||
|
|
||||||
|
Package: debian-image-builder-frontend
|
||||||
|
Architecture: all
|
||||||
|
Depends: \${misc:Depends}, nodejs, nginx
|
||||||
|
Description: Debian Image Builder Frontend
|
||||||
|
Web-based frontend for Debian Image Builder with Cockpit integration.
|
||||||
|
Provides a user interface for managing image builds, blueprints,
|
||||||
|
and system configurations through a modern React application.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > debian/rules << EOF
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
%:
|
||||||
|
dh \$@
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
dh_auto_install
|
||||||
|
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
||||||
|
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
||||||
|
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
||||||
|
|
||||||
|
# Copy built frontend files
|
||||||
|
if [ -d dist ]; then
|
||||||
|
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||||
|
elif [ -d build ]; then
|
||||||
|
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy source files for development
|
||||||
|
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||||
|
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||||
|
|
||||||
|
# Create nginx configuration
|
||||||
|
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/debian-image-builder-frontend;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files \$uri \$uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://localhost:8080/;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINX_EOF
|
||||||
|
|
||||||
|
# Create cockpit manifest
|
||||||
|
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"manifest": {
|
||||||
|
"name": "debian-image-builder",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Debian Image Builder",
|
||||||
|
"description": "Build and manage Debian atomic images",
|
||||||
|
"url": "/usr/share/debian-image-builder-frontend",
|
||||||
|
"icon": "debian-logo",
|
||||||
|
"requires": {
|
||||||
|
"cockpit": ">= 200"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
COCKPIT_EOF
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > debian/changelog << EOF
|
||||||
|
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release
|
||||||
|
* Debian Image Builder Frontend with Cockpit integration
|
||||||
|
* React-based web interface for image management
|
||||||
|
|
||||||
|
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > debian/compat << EOF
|
||||||
|
13
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x debian/rules
|
||||||
|
|
||||||
|
- name: Build Debian package
|
||||||
|
run: |
|
||||||
|
dpkg-buildpackage -us -uc -b
|
||||||
|
ls -la ../*.deb
|
||||||
|
|
||||||
|
- name: Upload Debian package
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: debian-image-builder-frontend-deb
|
||||||
|
path: ../*.deb
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
cockpit-integration:
|
||||||
|
name: Test Cockpit Integration
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:18-bullseye
|
||||||
|
needs: build-and-test
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Test cockpit integration
|
||||||
|
run: |
|
||||||
|
echo "Testing Cockpit integration..."
|
||||||
|
if [ -d cockpit ]; then
|
||||||
|
echo "Cockpit directory found:"
|
||||||
|
ls -la cockpit/
|
||||||
|
else
|
||||||
|
echo "No cockpit directory found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f package.json ]; then
|
||||||
|
echo "Package.json scripts:"
|
||||||
|
npm run
|
||||||
|
fi
|
||||||
257
.forgejo/workflows/ci.yml.disabled
Normal file
257
.forgejo/workflows/ci.yml.disabled
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
---
|
||||||
|
name: Debian Image Builder Frontend CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: "18"
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
name: Build and Test Frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:18-bullseye
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
git \
|
||||||
|
ca-certificates \
|
||||||
|
python3
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build || echo "Build script not found"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
if [ -f package.json ] && npm run test; then
|
||||||
|
npm test
|
||||||
|
else
|
||||||
|
echo "No test script found, skipping tests"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run linting
|
||||||
|
run: |
|
||||||
|
if [ -f package.json ] && npm run lint; then
|
||||||
|
npm run lint
|
||||||
|
else
|
||||||
|
echo "No lint script found, skipping linting"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build production bundle
|
||||||
|
run: |
|
||||||
|
if [ -f package.json ] && npm run build; then
|
||||||
|
npm run build
|
||||||
|
else
|
||||||
|
echo "No build script found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: frontend-build
|
||||||
|
path: |
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: Package Frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:18-bullseye
|
||||||
|
needs: build-and-test
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
devscripts \
|
||||||
|
debhelper \
|
||||||
|
git \
|
||||||
|
ca-certificates \
|
||||||
|
python3
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build production bundle
|
||||||
|
run: |
|
||||||
|
if [ -f package.json ] && npm run build; then
|
||||||
|
npm run build
|
||||||
|
else
|
||||||
|
echo "No build script found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create debian directory
|
||||||
|
run: |
|
||||||
|
mkdir -p debian
|
||||||
|
cat > debian/control << EOF
|
||||||
|
Source: debian-image-builder-frontend
|
||||||
|
Section: web
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Debian Forge Team <team@debian-forge.org>
|
||||||
|
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
||||||
|
Standards-Version: 4.6.2
|
||||||
|
|
||||||
|
Package: debian-image-builder-frontend
|
||||||
|
Architecture: all
|
||||||
|
Depends: \${misc:Depends}, nodejs, nginx
|
||||||
|
Description: Debian Image Builder Frontend
|
||||||
|
Web-based frontend for Debian Image Builder with Cockpit integration.
|
||||||
|
Provides a user interface for managing image builds, blueprints,
|
||||||
|
and system configurations through a modern React application.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > debian/rules << EOF
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
%:
|
||||||
|
dh \$@
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
dh_auto_install
|
||||||
|
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
||||||
|
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
||||||
|
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
||||||
|
|
||||||
|
# Copy built frontend files
|
||||||
|
if [ -d dist ]; then
|
||||||
|
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||||
|
elif [ -d build ]; then
|
||||||
|
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy source files for development
|
||||||
|
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||||
|
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||||
|
|
||||||
|
# Create nginx configuration
|
||||||
|
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/debian-image-builder-frontend;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files \$uri \$uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://localhost:8080/;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINX_EOF
|
||||||
|
|
||||||
|
# Create cockpit manifest
|
||||||
|
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"manifest": {
|
||||||
|
"name": "debian-image-builder",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Debian Image Builder",
|
||||||
|
"description": "Build and manage Debian atomic images",
|
||||||
|
"url": "/usr/share/debian-image-builder-frontend",
|
||||||
|
"icon": "debian-logo",
|
||||||
|
"requires": {
|
||||||
|
"cockpit": ">= 200"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
COCKPIT_EOF
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > debian/changelog << EOF
|
||||||
|
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release
|
||||||
|
* Debian Image Builder Frontend with Cockpit integration
|
||||||
|
* React-based web interface for image management
|
||||||
|
|
||||||
|
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > debian/compat << EOF
|
||||||
|
13
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x debian/rules
|
||||||
|
|
||||||
|
- name: Build Debian package
|
||||||
|
run: |
|
||||||
|
dpkg-buildpackage -us -uc -b
|
||||||
|
ls -la ../*.deb
|
||||||
|
|
||||||
|
- name: Upload Debian package
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: debian-image-builder-frontend-deb
|
||||||
|
path: ../*.deb
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
cockpit-integration:
|
||||||
|
name: Test Cockpit Integration
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:18-bullseye
|
||||||
|
needs: build-and-test
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Test cockpit integration
|
||||||
|
run: |
|
||||||
|
echo "Testing Cockpit integration..."
|
||||||
|
if [ -d cockpit ]; then
|
||||||
|
echo "Cockpit directory found:"
|
||||||
|
ls -la cockpit/
|
||||||
|
else
|
||||||
|
echo "No cockpit directory found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f package.json ]; then
|
||||||
|
echo "Package.json scripts:"
|
||||||
|
npm run
|
||||||
|
fi
|
||||||
|
|
@ -32,8 +32,7 @@ test:
|
||||||
- RUNNER:
|
- RUNNER:
|
||||||
- aws/fedora-41-x86_64
|
- aws/fedora-41-x86_64
|
||||||
- aws/fedora-42-x86_64
|
- aws/fedora-42-x86_64
|
||||||
- aws/rhel-9.6-nightly-x86_64
|
- aws/rhel-10.1-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:76bc23ca84c6b31251fee267f417ed262d9ea49655e942d1978149e137295bb0
|
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
||||||
- 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:09ca1ce263bb686b08187a3b836f6a5bd54875e8596105e4d7d816d57caf55a0
|
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
||||||
- 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:4bafcaab0f0c998a89a1cc33bdbbf74f39eea52e6c0e43013c356a322f94940f
|
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
||||||
- 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:5446102f233991bdc73451201adf361766d45c638f3d89f19121ae1c2ba8bf17
|
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
||||||
- 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:76bc23ca84c6b31251fee267f417ed262d9ea49655e942d1978149e137295bb0
|
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
||||||
- 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:09ca1ce263bb686b08187a3b836f6a5bd54875e8596105e4d7d816d57caf55a0
|
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
||||||
- 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:4bafcaab0f0c998a89a1cc33bdbbf74f39eea52e6c0e43013c356a322f94940f
|
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
||||||
- 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:5446102f233991bdc73451201adf361766d45c638f3d89f19121ae1c2ba8bf17
|
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
||||||
- 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: 74
|
Version: 76
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Image builder plugin for Cockpit
|
Summary: Image builder plugin for Cockpit
|
||||||
|
|
||||||
|
|
|
||||||
1848
package-lock.json
generated
1848
package-lock.json
generated
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
|
@ -8,16 +8,17 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ltd/j-toml": "1.38.0",
|
"@ltd/j-toml": "1.38.0",
|
||||||
"@patternfly/patternfly": "6.3.0",
|
"@patternfly/patternfly": "6.3.1",
|
||||||
"@patternfly/react-code-editor": "6.3.1",
|
"@patternfly/react-code-editor": "6.3.1",
|
||||||
"@patternfly/react-core": "6.3.0",
|
"@patternfly/react-core": "6.3.1",
|
||||||
"@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.3",
|
"@redhat-cloud-services/frontend-components-notifications": "6.1.5",
|
||||||
"@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.0",
|
"@sentry/webpack-plugin": "4.1.1",
|
||||||
"@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",
|
||||||
|
|
@ -46,13 +47,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.1.0",
|
"@types/node": "24.3.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.39.0",
|
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||||
"@typescript-eslint/parser": "8.39.0",
|
"@typescript-eslint/parser": "8.40.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",
|
||||||
|
|
@ -61,13 +62,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.32.0",
|
"eslint": "9.33.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.3",
|
"eslint-plugin-prettier": "5.5.4",
|
||||||
"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",
|
||||||
|
|
@ -80,7 +81,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.4",
|
"msw": "2.10.5",
|
||||||
"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",
|
||||||
|
|
@ -88,12 +89,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.0",
|
"stylelint": "16.23.1",
|
||||||
"stylelint-config-recommended-scss": "15.0.1",
|
"stylelint-config-recommended-scss": "16.0.0",
|
||||||
"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.38.0",
|
"typescript-eslint": "8.40.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,10 +22,8 @@ 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
|
||||||
|
|
|
||||||
214
playwright/Customizations/AAP.spec.ts
Normal file
214
playwright/Customizations/AAP.spec.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { test } from '../fixtures/customizations';
|
||||||
|
import { isHosted } from '../helpers/helpers';
|
||||||
|
import { ensureAuthenticated } from '../helpers/login';
|
||||||
|
import {
|
||||||
|
ibFrame,
|
||||||
|
navigateToLandingPage,
|
||||||
|
navigateToOptionalSteps,
|
||||||
|
} from '../helpers/navHelpers';
|
||||||
|
import {
|
||||||
|
createBlueprint,
|
||||||
|
deleteBlueprint,
|
||||||
|
exportBlueprint,
|
||||||
|
fillInDetails,
|
||||||
|
fillInImageOutputGuest,
|
||||||
|
importBlueprint,
|
||||||
|
registerLater,
|
||||||
|
} from '../helpers/wizardHelpers';
|
||||||
|
|
||||||
|
const validCallbackUrl =
|
||||||
|
'https://controller.url/api/controller/v2/job_templates/9/callback/';
|
||||||
|
const validHttpCallbackUrl =
|
||||||
|
'http://controller.url/api/controller/v2/job_templates/9/callback/';
|
||||||
|
const validHostConfigKey = 'hostconfigkey';
|
||||||
|
const validCertificate = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDXTCCAkWgAwIBAgIJAOEzx5ezZ9EIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||||
|
BAYTAklOMQswCQYDVQQIDAJLUjEMMAoGA1UEBwwDS1JHMRAwDgYDVQQKDAdUZXN0
|
||||||
|
IENBMB4XDTI1MDUxNTEyMDAwMFoXDTI2MDUxNTEyMDAwMFowRTELMAkGA1UEBhMC
|
||||||
|
SU4xCzAJBgNVBAgMAktSMQwwCgYDVQQHDANSR0sxEDAOBgNVBAoMB1Rlc3QgQ0Ew
|
||||||
|
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+R4gfN5pyJQo5qBTTtN+7
|
||||||
|
eE9CSXZJ8SVVaE3U54IgqQoqsSoBY5QtExy7v5C6l6mW4E6dzK/JecmvTTO/BvlG
|
||||||
|
A5k2hxB6bOQxtxYwfgElH+RFWN9P4xxhtEiQgHoG1rDfnXuDJk1U3YEkCQELUebz
|
||||||
|
fF3EIDU1yR0Sz2bA+Sl2VXe8og1MEZfytq8VZUVltxtn2PfW7zI5gOllBR2sKeUc
|
||||||
|
K6h8HXN7qMgfEvsLIXxTw7fU/zA3ibcxfRCl3m6QhF8hwRh6F9Wtz2s8hCzGegV5
|
||||||
|
z0M39nY7X8C3GZQ4Ly8v8DdY+FbEix7K3SSBRbWtdPfAHRFlX9Er2Wf8DAr7O2hH
|
||||||
|
AgMBAAGjUDBOMB0GA1UdDgQWBBTXXz2eIDgK+BhzDUAGzptn0OMcpDAfBgNVHSME
|
||||||
|
GDAWgBTXXz2eIDgK+BhzDUAGzptn0OMcpDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
|
||||||
|
DQEBCwUAA4IBAQAoUgY4jsuBMB3el9cc7JS2rcOhhJzn47Hj2UANfJq52g5lbjo7
|
||||||
|
XDc7Wb3VDcV+1LzjdzayT1qO1WzHb6FDPW9L9f6h4s8lj6MvJ+xhOWgD11srdIt3
|
||||||
|
vbQaQW4zDfeVRcKXzqbcUX8BLXAdzJPqVwZ+Z4EDjYrJ7lF9k+IqfZm0MsYX7el9
|
||||||
|
kvdRHbLuF4Q0sZ05CXMFkhM0Ulhu4MZ+1FcsQa7nWfZzTmbjHOuWJPB4z5WwrB7z
|
||||||
|
U8YYvWJ3qxToWGbATqJxkRKGGqLrNrmwcfzgPqkpuCRYi0Kky6gJ1RvL+DRopY9x
|
||||||
|
uD+ckf3oH2wYAB6RpPRMkfVxe7lGMvq/yEZ6
|
||||||
|
-----END CERTIFICATE-----`;
|
||||||
|
const invalidCertificate = `-----BEGIN CERTIFICATE-----
|
||||||
|
ThisIs*Not+Valid/Base64==
|
||||||
|
-----END CERTIFICATE-----`;
|
||||||
|
|
||||||
|
test('Create a blueprint with AAP registration customization', async ({
|
||||||
|
page,
|
||||||
|
cleanup,
|
||||||
|
}) => {
|
||||||
|
const blueprintName = 'test-' + uuidv4();
|
||||||
|
|
||||||
|
// Skip entirely in Cockpit/on-premise where AAP customization is unavailable
|
||||||
|
test.skip(!isHosted(), 'AAP customization is not available in the plugin');
|
||||||
|
|
||||||
|
// Delete the blueprint after the run fixture
|
||||||
|
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||||
|
await ensureAuthenticated(page);
|
||||||
|
|
||||||
|
// Navigate to IB landing page and get the frame
|
||||||
|
await navigateToLandingPage(page);
|
||||||
|
const frame = await ibFrame(page);
|
||||||
|
|
||||||
|
await test.step('Navigate to optional steps in Wizard', async () => {
|
||||||
|
await navigateToOptionalSteps(frame);
|
||||||
|
await registerLater(frame);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Select and fill the AAP step with valid configuration', async () => {
|
||||||
|
await frame
|
||||||
|
.getByRole('button', { name: 'Ansible Automation Platform' })
|
||||||
|
.click();
|
||||||
|
await frame
|
||||||
|
.getByRole('textbox', { name: 'ansible callback url' })
|
||||||
|
.fill(validCallbackUrl);
|
||||||
|
await frame
|
||||||
|
.getByRole('textbox', { name: 'host config key' })
|
||||||
|
.fill(validHostConfigKey);
|
||||||
|
await frame
|
||||||
|
.getByRole('textbox', { name: 'File upload' })
|
||||||
|
.fill(validCertificate);
|
||||||
|
await expect(frame.getByRole('button', { name: 'Next' })).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Test TLS confirmation checkbox for HTTPS URLs', async () => {
|
||||||
|
// TLS confirmation checkbox should appear for HTTPS URLs
|
||||||
|
await expect(
|
||||||
|
frame.getByRole('checkbox', {
|
||||||
|
name: 'Insecure',
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Check TLS confirmation and verify CA input is hidden
|
||||||
|
await frame
|
||||||
|
.getByRole('checkbox', {
|
||||||
|
name: 'Insecure',
|
||||||
|
})
|
||||||
|
.check();
|
||||||
|
await expect(
|
||||||
|
frame.getByRole('textbox', { name: 'File upload' }),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
|
await frame
|
||||||
|
.getByRole('checkbox', {
|
||||||
|
name: 'Insecure',
|
||||||
|
})
|
||||||
|
.uncheck();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
frame.getByRole('textbox', { name: 'File upload' }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Test certificate validation', async () => {
|
||||||
|
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
||||||
|
await frame
|
||||||
|
.getByRole('textbox', { name: 'File upload' })
|
||||||
|
.fill(invalidCertificate);
|
||||||
|
await expect(frame.getByText(/Certificate.*is not valid/)).toBeVisible();
|
||||||
|
|
||||||
|
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
||||||
|
await frame
|
||||||
|
.getByRole('textbox', { name: 'File upload' })
|
||||||
|
.fill(validCertificate);
|
||||||
|
|
||||||
|
await expect(frame.getByText('Certificate was uploaded')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Test HTTP URL behavior', async () => {
|
||||||
|
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
||||||
|
await frame
|
||||||
|
.getByRole('textbox', { name: 'ansible callback url' })
|
||||||
|
.fill(validHttpCallbackUrl);
|
||||||
|
|
||||||
|
// TLS confirmation checkbox should NOT appear for HTTP URLs
|
||||||
|
await expect(
|
||||||
|
frame.getByRole('checkbox', {
|
||||||
|
name: 'Insecure',
|
||||||
|
}),
|
||||||
|
).toBeHidden();
|
||||||
|
await expect(
|
||||||
|
frame.getByRole('textbox', { name: 'File upload' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
||||||
|
await frame
|
||||||
|
.getByRole('textbox', { name: 'ansible callback url' })
|
||||||
|
.fill(validCallbackUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Complete AAP configuration and proceed to review', async () => {
|
||||||
|
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Fill the BP details', async () => {
|
||||||
|
await fillInDetails(frame, blueprintName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Create BP', async () => {
|
||||||
|
await createBlueprint(frame, blueprintName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Edit BP and verify AAP configuration persists', async () => {
|
||||||
|
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
||||||
|
await frame.getByLabel('Revisit Ansible Automation Platform step').click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
frame.getByRole('textbox', { name: 'ansible callback url' }),
|
||||||
|
).toHaveValue(validCallbackUrl);
|
||||||
|
await expect(
|
||||||
|
frame.getByRole('textbox', { name: 'host config key' }),
|
||||||
|
).toHaveValue(validHostConfigKey);
|
||||||
|
await expect(
|
||||||
|
frame.getByRole('textbox', { name: 'File upload' }),
|
||||||
|
).toHaveValue(validCertificate);
|
||||||
|
|
||||||
|
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||||
|
await frame
|
||||||
|
.getByRole('button', { name: 'Save changes to blueprint' })
|
||||||
|
.click();
|
||||||
|
});
|
||||||
|
// This is for hosted service only as these features are not available in cockpit plugin
|
||||||
|
await test.step('Export BP', async (step) => {
|
||||||
|
step.skip(!isHosted(), 'Exporting is not available in the plugin');
|
||||||
|
await exportBlueprint(page, blueprintName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Import BP', async (step) => {
|
||||||
|
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
||||||
|
await importBlueprint(page, blueprintName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Review imported BP', async (step) => {
|
||||||
|
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
||||||
|
await fillInImageOutputGuest(page);
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Ansible Automation Platform' })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('textbox', { name: 'ansible callback url' }),
|
||||||
|
).toHaveValue(validCallbackUrl);
|
||||||
|
await expect(
|
||||||
|
page.getByRole('textbox', { name: 'host config key' }),
|
||||||
|
).toBeEmpty();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('textbox', { name: 'File upload' }),
|
||||||
|
).toHaveValue(validCertificate);
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -67,8 +67,10 @@ export const getHostDistroName = (): string => {
|
||||||
const lineData = l.split('=');
|
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']}`,
|
`${(osRel as any)['ID']}-${(osRel as any)['VERSION_ID'].split('.')[0]}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (distro === undefined) {
|
if (distro === undefined) {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,11 @@ 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();
|
||||||
|
|
@ -87,7 +92,12 @@ 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(blueprintName),
|
frame.locator('.pf-v6-c-card__title-text').getByText(
|
||||||
|
// if the name is too long, the blueprint card will have a truncated name.
|
||||||
|
blueprintName.length > 24
|
||||||
|
? blueprintName.slice(0, 24) + '...'
|
||||||
|
: blueprintName,
|
||||||
|
),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
7b4735d287dd0950e0a6f47dde65b62b0f239da1
|
cf0a810fd3b75fa27139746c4dfe72222e13dcba
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,21 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
|
||||||
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardTitle>
|
<CardTitle aria-label={blueprint.name}>
|
||||||
{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, useEffect, useState } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Bullseye,
|
Bullseye,
|
||||||
|
|
@ -17,7 +17,6 @@ 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';
|
||||||
|
|
||||||
|
|
@ -29,6 +28,7 @@ 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,16 +73,6 @@ 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, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -16,11 +16,13 @@ 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 { useComposeBPWithNotification as useComposeBlueprintMutation } from '../../Hooks';
|
import {
|
||||||
|
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';
|
||||||
|
|
@ -37,18 +39,7 @@ 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, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -9,14 +9,16 @@ 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 { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
|
import {
|
||||||
|
useDeleteBPWithNotification as useDeleteBlueprintMutation,
|
||||||
|
useGetUser,
|
||||||
|
} from '../../Hooks';
|
||||||
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
selectBlueprintSearchInput,
|
selectBlueprintSearchInput,
|
||||||
|
|
@ -42,17 +44,7 @@ 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, setUserData] = useState<ChromeUser | void>(undefined);
|
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 searchParams: GetBlueprintsApiArg = {
|
const searchParams: GetBlueprintsApiArg = {
|
||||||
limit: blueprintsLimit,
|
limit: blueprintsLimit,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { WizardStepType } from '@patternfly/react-core/dist/esm/components/Wizar
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import 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';
|
||||||
|
|
@ -40,6 +41,7 @@ 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,
|
||||||
|
|
@ -197,6 +199,7 @@ 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(() => {
|
||||||
|
|
@ -283,6 +286,8 @@ 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
|
||||||
|
|
@ -293,8 +298,10 @@ 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 = 22;
|
startIndex = JUMP_TO_REVIEW_STEP;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
|
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
|
||||||
|
|
@ -655,6 +662,22 @@ 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,6 +138,7 @@ 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);
|
||||||
|
|
||||||
|
|
@ -164,6 +165,7 @@ 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>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
DropEvent,
|
||||||
|
FileUpload,
|
||||||
|
FormGroup,
|
||||||
|
FormHelperText,
|
||||||
|
HelperText,
|
||||||
|
HelperTextItem,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
||||||
|
import {
|
||||||
|
changeAapCallbackUrl,
|
||||||
|
changeAapHostConfigKey,
|
||||||
|
changeAapTlsCertificateAuthority,
|
||||||
|
changeAapTlsConfirmation,
|
||||||
|
selectAapCallbackUrl,
|
||||||
|
selectAapHostConfigKey,
|
||||||
|
selectAapTlsCertificateAuthority,
|
||||||
|
selectAapTlsConfirmation,
|
||||||
|
} from '../../../../../store/wizardSlice';
|
||||||
|
import { useAAPValidation } from '../../../utilities/useValidation';
|
||||||
|
import { ValidatedInputAndTextArea } from '../../../ValidatedInput';
|
||||||
|
import { validateMultipleCertificates } from '../../../validators';
|
||||||
|
|
||||||
|
const AAPRegistration = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const callbackUrl = useAppSelector(selectAapCallbackUrl);
|
||||||
|
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
|
||||||
|
const tlsCertificateAuthority = useAppSelector(
|
||||||
|
selectAapTlsCertificateAuthority,
|
||||||
|
);
|
||||||
|
const tlsConfirmation = useAppSelector(selectAapTlsConfirmation);
|
||||||
|
const [isRejected, setIsRejected] = React.useState(false);
|
||||||
|
const stepValidation = useAAPValidation();
|
||||||
|
|
||||||
|
const isHttpsUrl = callbackUrl?.toLowerCase().startsWith('https://') || false;
|
||||||
|
const shouldShowCaInput = !isHttpsUrl || (isHttpsUrl && !tlsConfirmation);
|
||||||
|
|
||||||
|
const validated = stepValidation.errors['certificate']
|
||||||
|
? 'error'
|
||||||
|
: stepValidation.errors['certificate'] === undefined &&
|
||||||
|
tlsCertificateAuthority &&
|
||||||
|
validateMultipleCertificates(tlsCertificateAuthority).validCertificates
|
||||||
|
.length > 0
|
||||||
|
? 'success'
|
||||||
|
: 'default';
|
||||||
|
|
||||||
|
const handleCallbackUrlChange = (value: string) => {
|
||||||
|
dispatch(changeAapCallbackUrl(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHostConfigKeyChange = (value: string) => {
|
||||||
|
dispatch(changeAapHostConfigKey(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
dispatch(changeAapTlsCertificateAuthority(''));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTextChange = (
|
||||||
|
_event: React.ChangeEvent<HTMLTextAreaElement>,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
dispatch(changeAapTlsCertificateAuthority(value));
|
||||||
|
setIsRejected(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDataChange = (_: DropEvent, value: string) => {
|
||||||
|
dispatch(changeAapTlsCertificateAuthority(value));
|
||||||
|
setIsRejected(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileRejected = () => {
|
||||||
|
dispatch(changeAapTlsCertificateAuthority(''));
|
||||||
|
setIsRejected(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTlsConfirmationChange = (checked: boolean) => {
|
||||||
|
dispatch(changeAapTlsConfirmation(checked));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup label='Ansible Callback URL' isRequired>
|
||||||
|
<ValidatedInputAndTextArea
|
||||||
|
value={callbackUrl || ''}
|
||||||
|
onChange={(_event, value) => handleCallbackUrlChange(value.trim())}
|
||||||
|
ariaLabel='ansible callback url'
|
||||||
|
isRequired
|
||||||
|
stepValidation={stepValidation}
|
||||||
|
fieldName='callbackUrl'
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup label='Host Config Key' isRequired>
|
||||||
|
<ValidatedInputAndTextArea
|
||||||
|
value={hostConfigKey || ''}
|
||||||
|
onChange={(_event, value) => handleHostConfigKeyChange(value.trim())}
|
||||||
|
ariaLabel='host config key'
|
||||||
|
isRequired
|
||||||
|
stepValidation={stepValidation}
|
||||||
|
fieldName='hostConfigKey'
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{shouldShowCaInput && (
|
||||||
|
<FormGroup label='Certificate authority (CA) for Ansible Controller'>
|
||||||
|
<FileUpload
|
||||||
|
id='aap-certificate-upload'
|
||||||
|
type='text'
|
||||||
|
value={tlsCertificateAuthority || ''}
|
||||||
|
filename={tlsCertificateAuthority ? 'CA detected' : ''}
|
||||||
|
onDataChange={handleDataChange}
|
||||||
|
onTextChange={handleTextChange}
|
||||||
|
onClearClick={handleClear}
|
||||||
|
dropzoneProps={{
|
||||||
|
accept: {
|
||||||
|
'application/x-pem-file': ['.pem'],
|
||||||
|
'application/x-x509-ca-cert': ['.cer', '.crt'],
|
||||||
|
'application/pkix-cert': ['.der'],
|
||||||
|
},
|
||||||
|
maxSize: 512000,
|
||||||
|
onDropRejected: handleFileRejected,
|
||||||
|
}}
|
||||||
|
validated={isRejected ? 'error' : validated}
|
||||||
|
browseButtonText='Upload'
|
||||||
|
allowEditingUploadedText={true}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
<HelperText>
|
||||||
|
<HelperTextItem
|
||||||
|
variant={
|
||||||
|
isRejected || validated === 'error'
|
||||||
|
? 'error'
|
||||||
|
: validated === 'success'
|
||||||
|
? 'success'
|
||||||
|
: 'default'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isRejected
|
||||||
|
? 'Must be a .PEM/.CER/.CRT file'
|
||||||
|
: validated === 'error'
|
||||||
|
? stepValidation.errors['certificate']
|
||||||
|
: validated === 'success'
|
||||||
|
? 'Certificate was uploaded'
|
||||||
|
: 'Drag and drop a valid certificate file or upload one'}
|
||||||
|
</HelperTextItem>
|
||||||
|
</HelperText>
|
||||||
|
</FormHelperText>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
{isHttpsUrl && (
|
||||||
|
<FormGroup>
|
||||||
|
<Checkbox
|
||||||
|
id='tls-confirmation-checkbox'
|
||||||
|
label='Insecure'
|
||||||
|
isChecked={tlsConfirmation || false}
|
||||||
|
onChange={(_event, checked) => handleTlsConfirmationChange(checked)}
|
||||||
|
/>
|
||||||
|
{stepValidation.errors['tlsConfirmation'] && (
|
||||||
|
<FormHelperText>
|
||||||
|
<HelperText>
|
||||||
|
<HelperTextItem variant='error'>
|
||||||
|
{stepValidation.errors['tlsConfirmation']}
|
||||||
|
</HelperTextItem>
|
||||||
|
</HelperText>
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AAPRegistration;
|
||||||
18
src/Components/CreateImageWizard/steps/AAP/index.tsx
Normal file
18
src/Components/CreateImageWizard/steps/AAP/index.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Form, Title } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import AAPRegistration from './components/AAPRegistration';
|
||||||
|
|
||||||
|
const AAPStep = () => {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<Title headingLevel='h1' size='xl'>
|
||||||
|
Ansible Automation Platform
|
||||||
|
</Title>
|
||||||
|
<AAPRegistration />
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AAPStep;
|
||||||
|
|
@ -8,7 +8,10 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { useGetOscapCustomizationsQuery } from '../../../../../store/backendApi';
|
import {
|
||||||
|
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';
|
||||||
|
|
@ -16,6 +19,7 @@ import {
|
||||||
changeCompliance,
|
changeCompliance,
|
||||||
selectCompliancePolicyID,
|
selectCompliancePolicyID,
|
||||||
selectComplianceProfileID,
|
selectComplianceProfileID,
|
||||||
|
selectComplianceType,
|
||||||
selectDistribution,
|
selectDistribution,
|
||||||
selectFips,
|
selectFips,
|
||||||
} from '../../../../../store/wizardSlice';
|
} from '../../../../../store/wizardSlice';
|
||||||
|
|
@ -31,12 +35,29 @@ 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,
|
||||||
|
|
@ -48,6 +69,20 @@ export const OscapProfileInformation = ({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const customizationData =
|
||||||
|
compliancePolicyID && oscapPolicyInfo ? oscapPolicyInfo : oscapProfileInfo;
|
||||||
|
const profileMetadata = oscapProfileInfo;
|
||||||
|
const isPolicyDataLoading = compliancePolicyID
|
||||||
|
? isFetchingOscapPolicyInfo
|
||||||
|
: false;
|
||||||
|
const isFetchingOscapData = isPolicyDataLoading || isFetchingOscapProfileInfo;
|
||||||
|
const isPolicyDataSuccess = compliancePolicyID
|
||||||
|
? isSuccessOscapPolicyInfo
|
||||||
|
: true;
|
||||||
|
const isSuccessOscapData = isPolicyDataSuccess && isSuccessOscapProfileInfo;
|
||||||
|
const hasCriticalError = profileError || (compliancePolicyID && policyError);
|
||||||
|
const shouldShowData = isSuccessOscapData && !hasCriticalError;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: policyInfo,
|
data: policyInfo,
|
||||||
isFetching: isFetchingPolicyInfo,
|
isFetching: isFetchingPolicyInfo,
|
||||||
|
|
@ -74,23 +109,28 @@ export const OscapProfileInformation = ({
|
||||||
policyTitle: pol.title,
|
policyTitle: pol.title,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}, [isSuccessPolicyInfo]);
|
}, [isSuccessPolicyInfo, dispatch, policyInfo]);
|
||||||
|
|
||||||
const oscapProfile = oscapProfileInfo?.openscap as OpenScapProfile;
|
const oscapProfile = profileMetadata?.openscap as OpenScapProfile | undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(isFetchingOscapProfileInfo || isFetchingPolicyInfo) && (
|
{(isFetchingOscapData || isFetchingPolicyInfo) && <Spinner size='lg' />}
|
||||||
<Spinner size='lg' />
|
{hasCriticalError && (
|
||||||
|
<Content component={ContentVariants.p} className='pf-v6-u-color-200'>
|
||||||
|
Unable to load compliance information. Please try again.
|
||||||
|
</Content>
|
||||||
)}
|
)}
|
||||||
{isSuccessOscapProfileInfo && (
|
{shouldShowData && (
|
||||||
<>
|
<>
|
||||||
<Content component={ContentVariants.dl} className='review-step-dl'>
|
<Content component={ContentVariants.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'
|
||||||
>
|
>
|
||||||
Profile description
|
{complianceType === 'compliance'
|
||||||
|
? 'Policy description'
|
||||||
|
: 'Profile description'}
|
||||||
</Content>
|
</Content>
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
{oscapProfile?.profile_description}
|
{oscapProfile?.profile_description}
|
||||||
|
|
@ -116,7 +156,7 @@ export const OscapProfileInformation = ({
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeBlockCode>
|
<CodeBlockCode>
|
||||||
{(oscapProfileInfo?.packages ?? []).join(', ')}
|
{(customizationData?.packages ?? []).join(', ')}
|
||||||
</CodeBlockCode>
|
</CodeBlockCode>
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
@ -129,7 +169,7 @@ export const OscapProfileInformation = ({
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeBlockCode>
|
<CodeBlockCode>
|
||||||
{oscapProfileInfo?.kernel?.append}
|
{customizationData?.kernel?.append}
|
||||||
</CodeBlockCode>
|
</CodeBlockCode>
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
@ -142,7 +182,7 @@ export const OscapProfileInformation = ({
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeBlockCode>
|
<CodeBlockCode>
|
||||||
{(oscapProfileInfo?.services?.enabled ?? []).join(' ')}
|
{(customizationData?.services?.enabled ?? []).join(' ')}
|
||||||
</CodeBlockCode>
|
</CodeBlockCode>
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
@ -155,8 +195,8 @@ export const OscapProfileInformation = ({
|
||||||
<Content component={ContentVariants.dd}>
|
<Content component={ContentVariants.dd}>
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeBlockCode>
|
<CodeBlockCode>
|
||||||
{(oscapProfileInfo?.services?.disabled ?? [])
|
{(customizationData?.services?.disabled ?? [])
|
||||||
.concat(oscapProfileInfo?.services?.masked ?? [])
|
.concat(customizationData?.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 } = useGetOscapCustomizationsForPolicyQuery(
|
const { data: currentProfileData } = useGetComplianceCustomizationsQuery(
|
||||||
{
|
{
|
||||||
distribution: release,
|
distribution: release,
|
||||||
policy: policyID!,
|
policy: policyID!,
|
||||||
|
|
@ -105,7 +105,7 @@ const PolicySelector = () => {
|
||||||
{ skip: !policyID },
|
{ skip: !policyID },
|
||||||
);
|
);
|
||||||
|
|
||||||
const [trigger] = useLazyGetOscapCustomizationsForPolicyQuery();
|
const [trigger] = useLazyGetComplianceCustomizationsQuery();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!policies || policies.data === undefined) {
|
if (!policies || policies.data === undefined) {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ 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';
|
||||||
|
|
@ -66,7 +67,6 @@ 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 = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpackedData: IBPackageWithRepositoryInfo[] =
|
let 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,13 +724,16 @@ 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.sort((a, b) => {
|
unpackedData = orderBy(
|
||||||
if (a.name === b.name) {
|
unpackedData,
|
||||||
return (b.stream ?? '').localeCompare(a.stream ?? '');
|
[
|
||||||
} else {
|
'name',
|
||||||
return a.name.localeCompare(b.name);
|
(pkg) => pkg.stream || '',
|
||||||
}
|
(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) {
|
||||||
|
|
@ -866,8 +869,6 @@ 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 || '',
|
||||||
|
|
@ -993,7 +994,18 @@ const Packages = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialExpandedPkgs: IBPackageWithRepositoryInfo[] = [];
|
const getPackageUniqueKey = (pkg: IBPackageWithRepositoryInfo): string => {
|
||||||
|
try {
|
||||||
|
if (!pkg || !pkg.name) {
|
||||||
|
return `invalid_${Date.now()}`;
|
||||||
|
}
|
||||||
|
return `${pkg.name}_${pkg.stream || 'none'}_${pkg.module_name || 'none'}_${pkg.repository || 'unknown'}`;
|
||||||
|
} catch {
|
||||||
|
return `error_${Date.now()}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialExpandedPkgs: string[] = [];
|
||||||
const [expandedPkgs, setExpandedPkgs] = useState(initialExpandedPkgs);
|
const [expandedPkgs, setExpandedPkgs] = useState(initialExpandedPkgs);
|
||||||
|
|
||||||
const setPkgExpanded = (
|
const setPkgExpanded = (
|
||||||
|
|
@ -1001,12 +1013,13 @@ const Packages = () => {
|
||||||
isExpanding: boolean,
|
isExpanding: boolean,
|
||||||
) =>
|
) =>
|
||||||
setExpandedPkgs((prevExpanded) => {
|
setExpandedPkgs((prevExpanded) => {
|
||||||
const otherExpandedPkgs = prevExpanded.filter((p) => p.name !== pkg.name);
|
const pkgKey = getPackageUniqueKey(pkg);
|
||||||
return isExpanding ? [...otherExpandedPkgs, pkg] : otherExpandedPkgs;
|
const otherExpandedPkgs = prevExpanded.filter((key) => key !== pkgKey);
|
||||||
|
return isExpanding ? [...otherExpandedPkgs, pkgKey] : otherExpandedPkgs;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
||||||
expandedPkgs.includes(pkg);
|
expandedPkgs.includes(getPackageUniqueKey(pkg));
|
||||||
|
|
||||||
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
||||||
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
||||||
|
|
@ -1030,51 +1043,37 @@ const Packages = () => {
|
||||||
'asc' | 'desc'
|
'asc' | 'desc'
|
||||||
>('asc');
|
>('asc');
|
||||||
|
|
||||||
const getSortableRowValues = (
|
const sortedPackages = useMemo(() => {
|
||||||
pkg: IBPackageWithRepositoryInfo,
|
if (!transformedPackages || !Array.isArray(transformedPackages)) {
|
||||||
): (string | number | ApiPackageSourcesResponse[] | undefined)[] => {
|
return [];
|
||||||
return [pkg.name, pkg.summary, pkg.stream, pkg.end_date, pkg.repository];
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let sortedPackages = transformedPackages;
|
return orderBy(
|
||||||
sortedPackages = transformedPackages.sort((a, b) => {
|
transformedPackages,
|
||||||
const aValue = getSortableRowValues(a)[activeSortIndex];
|
[
|
||||||
const bValue = getSortableRowValues(b)[activeSortIndex];
|
// Active stream packages first (if activeStream is set)
|
||||||
if (typeof aValue === 'number') {
|
(pkg) => (activeStream && pkg.stream === activeStream ? 0 : 1),
|
||||||
// Numeric sort
|
// Then by name
|
||||||
if (activeSortDirection === 'asc') {
|
'name',
|
||||||
return (aValue as number) - (bValue as number);
|
// Then by stream version (descending)
|
||||||
}
|
(pkg) => {
|
||||||
return (bValue as number) - (aValue as number);
|
if (!pkg.stream) return '';
|
||||||
}
|
const parts = pkg.stream
|
||||||
// String sort
|
.split('.')
|
||||||
// if active stream is set, sort it to the top
|
.map((part) => parseInt(part, 10) || 0);
|
||||||
if (aValue === activeStream) {
|
// Convert to string with zero-padding for proper sorting
|
||||||
return -1;
|
return parts.map((p) => p.toString().padStart(10, '0')).join('.');
|
||||||
}
|
},
|
||||||
if (bValue === activeStream) {
|
// Then by end date (nulls last)
|
||||||
return 1;
|
(pkg) => pkg.end_date || '9999-12-31',
|
||||||
}
|
// Then by repository
|
||||||
if (activeSortDirection === 'asc') {
|
(pkg) => pkg.repository || '',
|
||||||
// handle packages with undefined stream
|
// Finally by module name
|
||||||
if (!aValue) {
|
(pkg) => pkg.module_name || '',
|
||||||
return -1;
|
],
|
||||||
}
|
['asc', 'asc', 'desc', 'asc', 'asc', 'asc'],
|
||||||
if (!bValue) {
|
);
|
||||||
return 1;
|
}, [transformedPackages, activeStream]);
|
||||||
}
|
|
||||||
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: {
|
||||||
|
|
@ -1100,14 +1099,14 @@ const Packages = () => {
|
||||||
(module) => module.name === pkg.name,
|
(module) => module.name === pkg.name,
|
||||||
);
|
);
|
||||||
isSelected =
|
isSelected =
|
||||||
packages.some((p) => p.name === pkg.name) && !isModuleWithSameName;
|
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
||||||
|
!isModuleWithSameName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pkg.type === 'module') {
|
if (pkg.type === 'module') {
|
||||||
// the package is selected if it's added to the packages state
|
// the package is selected if its module stream matches one in enabled_modules
|
||||||
// and its module stream matches one in enabled_modules
|
|
||||||
isSelected =
|
isSelected =
|
||||||
packages.some((p) => p.name === pkg.name) &&
|
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
||||||
modules.some(
|
modules.some(
|
||||||
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
|
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
|
||||||
);
|
);
|
||||||
|
|
@ -1208,7 +1207,7 @@ const Packages = () => {
|
||||||
.slice(computeStart(), computeEnd())
|
.slice(computeStart(), computeEnd())
|
||||||
.map((grp, rowIndex) => (
|
.map((grp, rowIndex) => (
|
||||||
<Tbody
|
<Tbody
|
||||||
key={`${grp.name}-${rowIndex}`}
|
key={`${grp.name}-${grp.repository || 'default'}`}
|
||||||
isExpanded={isGroupExpanded(grp.name)}
|
isExpanded={isGroupExpanded(grp.name)}
|
||||||
>
|
>
|
||||||
<Tr data-testid='package-row'>
|
<Tr data-testid='package-row'>
|
||||||
|
|
@ -1308,7 +1307,7 @@ const Packages = () => {
|
||||||
.slice(computeStart(), computeEnd())
|
.slice(computeStart(), computeEnd())
|
||||||
.map((pkg, rowIndex) => (
|
.map((pkg, rowIndex) => (
|
||||||
<Tbody
|
<Tbody
|
||||||
key={`${pkg.name}-${rowIndex}`}
|
key={`${pkg.name}-${pkg.stream || 'default'}-${pkg.module_name || pkg.name}`}
|
||||||
isExpanded={isPkgExpanded(pkg)}
|
isExpanded={isPkgExpanded(pkg)}
|
||||||
>
|
>
|
||||||
<Tr data-testid='package-row'>
|
<Tr data-testid='package-row'>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopy,
|
ClipboardCopy,
|
||||||
|
|
@ -16,6 +16,7 @@ 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,
|
||||||
|
|
@ -24,18 +25,7 @@ import {
|
||||||
|
|
||||||
const RegistrationStep = () => {
|
const RegistrationStep = () => {
|
||||||
const { auth } = useChrome();
|
const { auth } = useChrome();
|
||||||
const [orgId, setOrgId] = useState<string | undefined>(undefined);
|
const { orgId } = useGetUser(auth);
|
||||||
|
|
||||||
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, { useEffect, useState } from 'react';
|
import React, { 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,19 +44,8 @@ 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);
|
||||||
|
|
||||||
|
|
@ -113,17 +102,7 @@ export const CreateSaveButton = ({
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: CreateDropdownProps) => {
|
}: CreateDropdownProps) => {
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = useChrome();
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React 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,19 +37,8 @@ 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);
|
||||||
|
|
@ -105,19 +94,8 @@ 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,6 +18,7 @@ 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';
|
||||||
|
|
@ -33,6 +34,7 @@ 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();
|
||||||
|
|
@ -52,14 +54,12 @@ 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 should be fine on-prem, we should
|
// NOTE: This is fine for on prem because we save the org id
|
||||||
// be able to ignore the `org-id`
|
// to state through a form field in the registration step
|
||||||
return mapRequestFromState(store, '');
|
return mapRequestFromState(store, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
KernelList,
|
KernelList,
|
||||||
LocaleList,
|
LocaleList,
|
||||||
OscapList,
|
OscapList,
|
||||||
|
RegisterAapList,
|
||||||
RegisterLaterList,
|
RegisterLaterList,
|
||||||
RegisterNowList,
|
RegisterNowList,
|
||||||
RegisterSatelliteList,
|
RegisterSatelliteList,
|
||||||
|
|
@ -42,6 +43,7 @@ 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,
|
||||||
|
|
@ -65,6 +67,7 @@ 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);
|
||||||
|
|
@ -83,6 +86,7 @@ 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);
|
||||||
|
|
@ -101,6 +105,8 @@ 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) =>
|
||||||
|
|
@ -499,6 +505,21 @@ 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,6 +41,10 @@ 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,
|
||||||
|
|
@ -660,6 +664,45 @@ export const RegisterSatelliteList = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const RegisterAapList = () => {
|
||||||
|
const callbackUrl = useAppSelector(selectAapCallbackUrl);
|
||||||
|
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
|
||||||
|
const tlsCertificateAuthority = useAppSelector(
|
||||||
|
selectAapTlsCertificateAuthority,
|
||||||
|
);
|
||||||
|
const skipTlsVerification = useAppSelector(selectAapTlsConfirmation);
|
||||||
|
|
||||||
|
const getTlsStatus = () => {
|
||||||
|
if (skipTlsVerification) {
|
||||||
|
return 'Insecure (TLS verification skipped)';
|
||||||
|
}
|
||||||
|
return tlsCertificateAuthority ? 'Configured' : 'None';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Content>
|
||||||
|
<Content component={ContentVariants.dl} className='review-step-dl'>
|
||||||
|
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
|
||||||
|
Ansible Callback URL
|
||||||
|
</Content>
|
||||||
|
<Content component={ContentVariants.dd}>
|
||||||
|
{callbackUrl || 'None'}
|
||||||
|
</Content>
|
||||||
|
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
|
||||||
|
Host Config Key
|
||||||
|
</Content>
|
||||||
|
<Content component={ContentVariants.dd}>
|
||||||
|
{hostConfigKey || 'None'}
|
||||||
|
</Content>
|
||||||
|
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
|
||||||
|
TLS Certificate
|
||||||
|
</Content>
|
||||||
|
<Content component={ContentVariants.dd}>{getTlsStatus()}</Content>
|
||||||
|
</Content>
|
||||||
|
</Content>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const RegisterNowList = () => {
|
export const RegisterNowList = () => {
|
||||||
const activationKey = useAppSelector(selectActivationKey);
|
const activationKey = useAppSelector(selectActivationKey);
|
||||||
const registrationType = useAppSelector(selectRegistrationType);
|
const registrationType = useAppSelector(selectRegistrationType);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
CockpitUploadTypes,
|
CockpitUploadTypes,
|
||||||
} from '../../../store/cockpit/types';
|
} from '../../../store/cockpit/types';
|
||||||
import {
|
import {
|
||||||
|
AapRegistration,
|
||||||
AwsUploadRequestOptions,
|
AwsUploadRequestOptions,
|
||||||
AzureUploadRequestOptions,
|
AzureUploadRequestOptions,
|
||||||
BlueprintExportResponse,
|
BlueprintExportResponse,
|
||||||
|
|
@ -49,6 +50,11 @@ import { ApiRepositoryImportResponseRead } from '../../../store/service/contentS
|
||||||
import {
|
import {
|
||||||
ComplianceType,
|
ComplianceType,
|
||||||
initialState,
|
initialState,
|
||||||
|
RegistrationType,
|
||||||
|
selectAapCallbackUrl,
|
||||||
|
selectAapHostConfigKey,
|
||||||
|
selectAapTlsCertificateAuthority,
|
||||||
|
selectAapTlsConfirmation,
|
||||||
selectActivationKey,
|
selectActivationKey,
|
||||||
selectArchitecture,
|
selectArchitecture,
|
||||||
selectAwsAccountId,
|
selectAwsAccountId,
|
||||||
|
|
@ -205,8 +211,9 @@ 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;
|
.options as AwsUploadRequestOptions & { region?: string | undefined };
|
||||||
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
|
||||||
|
|
@ -309,6 +316,7 @@ 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,
|
||||||
|
|
@ -387,14 +395,7 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
||||||
baseUrl: request.customizations.subscription?.['base-url'] || '',
|
baseUrl: request.customizations.subscription?.['base-url'] || '',
|
||||||
},
|
},
|
||||||
registration: {
|
registration: {
|
||||||
registrationType:
|
registrationType: getRegistrationType(request),
|
||||||
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,
|
||||||
|
|
@ -403,6 +404,15 @@ 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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -452,6 +462,15 @@ 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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -461,6 +480,24 @@ 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[] => {
|
||||||
|
|
@ -482,6 +519,24 @@ const getImageRequests = (
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRegistrationType = (request: BlueprintResponse): RegistrationType => {
|
||||||
|
const subscription = request.customizations.subscription;
|
||||||
|
const distribution = request.distribution;
|
||||||
|
const files = request.customizations.files;
|
||||||
|
|
||||||
|
if (subscription && isRhel(distribution)) {
|
||||||
|
if (subscription.rhc) {
|
||||||
|
return 'register-now-rhc';
|
||||||
|
} else {
|
||||||
|
return 'register-now-insights';
|
||||||
|
}
|
||||||
|
} else if (getSatelliteCommand(files)) {
|
||||||
|
return 'register-satellite';
|
||||||
|
} else {
|
||||||
|
return 'register-later';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getSatelliteCommand = (files?: File[]): string => {
|
const getSatelliteCommand = (files?: File[]): string => {
|
||||||
const satelliteCommandFile = files?.find(
|
const satelliteCommandFile = files?.find(
|
||||||
(file) => file.path === SATELLITE_PATH,
|
(file) => file.path === SATELLITE_PATH,
|
||||||
|
|
@ -642,6 +697,7 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
|
||||||
pem_certs: [satCert],
|
pem_certs: [satCert],
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
aap_registration: getAapRegistration(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ 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,
|
||||||
|
|
@ -54,6 +58,8 @@ import {
|
||||||
isSshKeyValid,
|
isSshKeyValid,
|
||||||
isUserGroupValid,
|
isUserGroupValid,
|
||||||
isUserNameValid,
|
isUserNameValid,
|
||||||
|
isValidUrl,
|
||||||
|
validateMultipleCertificates,
|
||||||
} from '../validators';
|
} from '../validators';
|
||||||
|
|
||||||
export type StepValidation = {
|
export type StepValidation = {
|
||||||
|
|
@ -205,6 +211,62 @@ 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,3 +138,85 @@ 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, { useEffect, useState } from 'react';
|
import React 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 />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseGcpSharedWith = (
|
export const parseGcpSharedWith = (
|
||||||
sharedWith: GcpUploadRequestOptions['share_with_accounts'],
|
sharedWith: GcpUploadRequestOptions['share_with_accounts'],
|
||||||
) => {
|
) => {
|
||||||
if (sharedWith) {
|
if (sharedWith) {
|
||||||
|
|
@ -134,19 +134,9 @@ 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 { ChromeUser } from '@redhat-cloud-services/types';
|
import { useFlag } from '@unleash/proxy-client-react';
|
||||||
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,6 +58,7 @@ 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,
|
||||||
|
|
@ -87,11 +88,12 @@ 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 =
|
||||||
|
|
@ -104,16 +106,7 @@ 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,
|
||||||
|
|
@ -382,8 +375,14 @@ 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 = <CloudInstance compose={compose} />;
|
const instance = launchEofFlag ? (
|
||||||
|
<AzureLaunchModal compose={compose} />
|
||||||
|
) : (
|
||||||
|
<CloudInstance compose={compose} />
|
||||||
|
);
|
||||||
const status = <CloudStatus compose={compose} />;
|
const status = <CloudStatus compose={compose} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -403,13 +402,18 @@ 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 = <OciInstance compose={compose} isExpired={isExpired} />;
|
const instance = launchEofFlag ? (
|
||||||
|
<OciLaunchModal compose={compose} isExpired={isExpired} />
|
||||||
|
) : (
|
||||||
|
<OciInstance compose={compose} isExpired={isExpired} />
|
||||||
|
);
|
||||||
const status = (
|
const status = (
|
||||||
<ExpiringStatus
|
<ExpiringStatus
|
||||||
compose={compose}
|
compose={compose}
|
||||||
|
|
@ -467,18 +471,8 @@ 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} />;
|
||||||
|
|
@ -553,18 +547,8 @@ 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, useEffect, useState } from 'react';
|
import React, { Suspense, useState } from 'react';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
|
@ -20,7 +20,6 @@ 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';
|
||||||
|
|
@ -31,6 +30,7 @@ import {
|
||||||
MODAL_ANCHOR,
|
MODAL_ANCHOR,
|
||||||
SEARCH_INPUT,
|
SEARCH_INPUT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import { useGetUser } from '../../Hooks';
|
||||||
import {
|
import {
|
||||||
useGetBlueprintsQuery,
|
useGetBlueprintsQuery,
|
||||||
useGetComposeStatusQuery,
|
useGetComposeStatusQuery,
|
||||||
|
|
@ -54,7 +54,10 @@ 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;
|
||||||
|
|
@ -97,21 +100,12 @@ const ProvisioningLink = ({
|
||||||
compose,
|
compose,
|
||||||
composeStatus,
|
composeStatus,
|
||||||
}: ProvisioningLinkPropTypes) => {
|
}: ProvisioningLinkPropTypes) => {
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
const launchEofFlag = useFlag('image-builder.launcheof');
|
||||||
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
|
|
||||||
useEffect(() => {
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
(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',
|
||||||
|
|
@ -182,7 +176,7 @@ const ProvisioningLink = ({
|
||||||
account_id: userData?.identity.internal?.account_id || 'Not found',
|
account_id: userData?.identity.internal?.account_id || 'Not found',
|
||||||
});
|
});
|
||||||
|
|
||||||
setWizardOpen(true);
|
setIsModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Launch
|
Launch
|
||||||
|
|
@ -202,6 +196,10 @@ const ProvisioningLink = ({
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
|
||||||
|
setIsModalOpen(!isModalOpen);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Suspense fallback='loading...'>
|
<Suspense fallback='loading...'>
|
||||||
|
|
@ -209,7 +207,23 @@ const ProvisioningLink = ({
|
||||||
compose.blueprint_version !== selectedBlueprintVersion
|
compose.blueprint_version !== selectedBlueprintVersion
|
||||||
? buttonWithTooltip
|
? buttonWithTooltip
|
||||||
: btn}
|
: btn}
|
||||||
{wizardOpen && (
|
{launchEofFlag && isModalOpen && provider === 'aws' && (
|
||||||
|
<AWSLaunchModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
handleModalToggle={handleModalToggle}
|
||||||
|
compose={compose}
|
||||||
|
composeStatus={composeStatus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{launchEofFlag && isModalOpen && provider === 'gcp' && (
|
||||||
|
<GcpLaunchModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
handleModalToggle={handleModalToggle}
|
||||||
|
compose={compose}
|
||||||
|
composeStatus={composeStatus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!launchEofFlag && isModalOpen && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen
|
isOpen
|
||||||
appendTo={appendTo}
|
appendTo={appendTo}
|
||||||
|
|
@ -218,7 +232,7 @@ const ProvisioningLink = ({
|
||||||
>
|
>
|
||||||
<ProvisioningWizard
|
<ProvisioningWizard
|
||||||
hasAccess={permissions[provider]}
|
hasAccess={permissions[provider]}
|
||||||
onClose={() => setWizardOpen(false)}
|
onClose={() => setIsModalOpen(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, { useEffect, useState } from 'react';
|
import React 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,18 +122,8 @@ 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 />;
|
||||||
|
|
|
||||||
104
src/Components/Launch/AWSLaunchModal.tsx
Normal file
104
src/Components/Launch/AWSLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
ListComponent,
|
||||||
|
ListItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalVariant,
|
||||||
|
OrderType,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ComposesResponseItem,
|
||||||
|
ComposeStatus,
|
||||||
|
} from '../../store/imageBuilderApi';
|
||||||
|
import { isAwsUploadRequestOptions } from '../../store/typeGuards';
|
||||||
|
|
||||||
|
type LaunchProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleModalToggle: (event: KeyboardEvent | React.MouseEvent) => void;
|
||||||
|
compose: ComposesResponseItem;
|
||||||
|
composeStatus: ComposeStatus | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AWSLaunchModal = ({
|
||||||
|
isOpen,
|
||||||
|
handleModalToggle,
|
||||||
|
compose,
|
||||||
|
composeStatus,
|
||||||
|
}: LaunchProps) => {
|
||||||
|
const options = compose.request.image_requests[0].upload_request.options;
|
||||||
|
|
||||||
|
if (!isAwsUploadRequestOptions(options)) {
|
||||||
|
throw TypeError(
|
||||||
|
`Error: options must be of type AwsUploadRequestOptions, not ${typeof options}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const amiId =
|
||||||
|
composeStatus?.image_status.status === 'success' &&
|
||||||
|
composeStatus.image_status.upload_status?.options &&
|
||||||
|
'ami' in composeStatus.image_status.upload_status.options
|
||||||
|
? composeStatus.image_status.upload_status.options.ami
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleModalToggle}
|
||||||
|
variant={ModalVariant.large}
|
||||||
|
aria-label='Open launch guide modal'
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
title={'Launch with Amazon Web Services'}
|
||||||
|
labelId='modal-title'
|
||||||
|
description={compose.image_name}
|
||||||
|
/>
|
||||||
|
<ModalBody id='modal-box-body-basic'>
|
||||||
|
<List component={ListComponent.ol} type={OrderType.number}>
|
||||||
|
<ListItem>
|
||||||
|
Navigate to the{' '}
|
||||||
|
<Button
|
||||||
|
component='a'
|
||||||
|
target='_blank'
|
||||||
|
variant='link'
|
||||||
|
icon={<ExternalLinkAltIcon />}
|
||||||
|
iconPosition='right'
|
||||||
|
href={`https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#ImageDetails:imageId=${amiId}`}
|
||||||
|
className='pf-v6-u-pl-0'
|
||||||
|
>
|
||||||
|
Images detail page
|
||||||
|
</Button>{' '}
|
||||||
|
located on your AWS console.
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
Copy the image to make it a permanent copy in your account.
|
||||||
|
<br />
|
||||||
|
Shared with Account{' '}
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>
|
||||||
|
{options.share_with_accounts?.[0]}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
AMI ID: <span className='pf-v6-u-font-weight-bold'>{amiId}</span>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>Launch image as an instance.</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
Connect to it via SSH using the following username:{' '}
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>ec2-user</span>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
116
src/Components/Launch/AzureLaunchModal.tsx
Normal file
116
src/Components/Launch/AzureLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import React, { Fragment, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
ListComponent,
|
||||||
|
ListItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalVariant,
|
||||||
|
OrderType,
|
||||||
|
Skeleton,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ComposesResponseItem,
|
||||||
|
useGetComposeStatusQuery,
|
||||||
|
} from '../../store/imageBuilderApi';
|
||||||
|
import { isAzureUploadStatus } from '../../store/typeGuards';
|
||||||
|
|
||||||
|
type LaunchProps = {
|
||||||
|
compose: ComposesResponseItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AzureLaunchModal = ({ compose }: LaunchProps) => {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const { data, isSuccess, isFetching } = useGetComposeStatusQuery({
|
||||||
|
composeId: compose.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isSuccess) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = data?.image_status.upload_status?.options;
|
||||||
|
|
||||||
|
if (options && !isAzureUploadStatus(options)) {
|
||||||
|
throw TypeError(
|
||||||
|
`Error: options must be of type AzureUploadStatus, not ${typeof options}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
|
||||||
|
setIsModalOpen(!isModalOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
isInline
|
||||||
|
isDisabled={data?.image_status.status !== 'success'}
|
||||||
|
onClick={handleModalToggle}
|
||||||
|
>
|
||||||
|
Launch
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleModalToggle}
|
||||||
|
variant={ModalVariant.large}
|
||||||
|
aria-label='Open launch guide wizard'
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
title={'Launch with Microsoft Azure'}
|
||||||
|
labelId='modal-title'
|
||||||
|
description={compose.image_name}
|
||||||
|
/>
|
||||||
|
<ModalBody id='modal-box-body-basic'>
|
||||||
|
<List component={ListComponent.ol} type={OrderType.number}>
|
||||||
|
<ListItem>
|
||||||
|
Locate{' '}
|
||||||
|
{!isFetching && (
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>
|
||||||
|
{options?.image_name}{' '}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isFetching && <Skeleton />}
|
||||||
|
in the{' '}
|
||||||
|
<Button
|
||||||
|
component='a'
|
||||||
|
target='_blank'
|
||||||
|
variant='link'
|
||||||
|
icon={<ExternalLinkAltIcon />}
|
||||||
|
iconPosition='right'
|
||||||
|
href={`https://portal.azure.com/#view/Microsoft_Azure_ComputeHub/ComputeHubMenuBlade/~/imagesBrowse`}
|
||||||
|
className='pf-v6-u-pl-0'
|
||||||
|
>
|
||||||
|
Azure console
|
||||||
|
</Button>
|
||||||
|
.
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
Create a Virtual Machine (VM) by using the image.
|
||||||
|
<br />
|
||||||
|
Note: Review the{' '}
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>
|
||||||
|
Availability Zone
|
||||||
|
</span>{' '}
|
||||||
|
and the <span className='pf-v6-u-font-weight-bold'>Size</span> to
|
||||||
|
meet your requirements. Adjust these settings as needed.
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
169
src/Components/Launch/GcpLaunchModal.tsx
Normal file
169
src/Components/Launch/GcpLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ClipboardCopy,
|
||||||
|
ClipboardCopyVariant,
|
||||||
|
List,
|
||||||
|
ListComponent,
|
||||||
|
ListItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalVariant,
|
||||||
|
OrderType,
|
||||||
|
TextInput,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import { generateDefaultName } from './useGenerateDefaultName';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ComposesResponseItem,
|
||||||
|
ComposeStatus,
|
||||||
|
} from '../../store/imageBuilderApi';
|
||||||
|
import {
|
||||||
|
isGcpUploadRequestOptions,
|
||||||
|
isGcpUploadStatus,
|
||||||
|
} from '../../store/typeGuards';
|
||||||
|
import { parseGcpSharedWith } from '../ImagesTable/ImageDetails';
|
||||||
|
|
||||||
|
type LaunchProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleModalToggle: (event: KeyboardEvent | React.MouseEvent) => void;
|
||||||
|
compose: ComposesResponseItem;
|
||||||
|
composeStatus: ComposeStatus | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GcpLaunchModal = ({
|
||||||
|
isOpen,
|
||||||
|
handleModalToggle,
|
||||||
|
compose,
|
||||||
|
composeStatus,
|
||||||
|
}: LaunchProps) => {
|
||||||
|
const [customerProjectId, setCustomerProjectId] = useState('');
|
||||||
|
|
||||||
|
const statusOptions = composeStatus?.image_status.upload_status?.options;
|
||||||
|
const composeOptions =
|
||||||
|
compose.request.image_requests[0].upload_request.options;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(statusOptions && !isGcpUploadStatus(statusOptions)) ||
|
||||||
|
!isGcpUploadRequestOptions(composeOptions)
|
||||||
|
) {
|
||||||
|
throw TypeError(
|
||||||
|
`Error: options must be of type GcpUploadRequestOptions, not ${typeof statusOptions}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageName = statusOptions?.image_name;
|
||||||
|
const projectId = statusOptions?.project_id;
|
||||||
|
if (!imageName || !projectId) {
|
||||||
|
throw TypeError(
|
||||||
|
`Error: Image name not found, unable to generate a command to copy ${typeof statusOptions}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const uniqueImageName = generateDefaultName(imageName);
|
||||||
|
const authorizeString =
|
||||||
|
composeOptions.share_with_accounts &&
|
||||||
|
composeOptions.share_with_accounts.length === 1
|
||||||
|
? `Authorize gcloud CLI to the following
|
||||||
|
account: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.`
|
||||||
|
: composeOptions.share_with_accounts
|
||||||
|
? `Authorize gcloud CLI to use one of the following
|
||||||
|
accounts: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.`
|
||||||
|
: 'Authorize gcloud CLI to use the account that the image is shared with.';
|
||||||
|
const installationCommand = `sudo dnf install google-cloud-cli`;
|
||||||
|
const createImage = `gcloud compute images create ${uniqueImageName} --source-image=${imageName} --source-image-project=${projectId} --project=${
|
||||||
|
customerProjectId || '<your_project_id>'
|
||||||
|
}`;
|
||||||
|
const createInstance = `gcloud compute instances create ${uniqueImageName} --image=${uniqueImageName} --project=${
|
||||||
|
customerProjectId || '<your_project_id>'
|
||||||
|
}`;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleModalToggle}
|
||||||
|
variant={ModalVariant.large}
|
||||||
|
aria-label='Open launch guide modal'
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
title={'Launch with Google Cloud Platform'}
|
||||||
|
labelId='modal-title'
|
||||||
|
description={compose.image_name}
|
||||||
|
/>
|
||||||
|
<ModalBody id='modal-box-body-basic'>
|
||||||
|
<List component={ListComponent.ol} type={OrderType.number}>
|
||||||
|
<ListItem>
|
||||||
|
Install the gcloud CLI. See the{' '}
|
||||||
|
<Button
|
||||||
|
component='a'
|
||||||
|
target='_blank'
|
||||||
|
variant='link'
|
||||||
|
icon={<ExternalLinkAltIcon />}
|
||||||
|
iconPosition='right'
|
||||||
|
href={`https://cloud.google.com/sdk/docs/install`}
|
||||||
|
className='pf-v6-u-pl-0'
|
||||||
|
>
|
||||||
|
Install gcloud CLI
|
||||||
|
</Button>
|
||||||
|
documentation.
|
||||||
|
<ClipboardCopy isReadOnly hoverTip='Copy' clickTip='Copied'>
|
||||||
|
{installationCommand}
|
||||||
|
</ClipboardCopy>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>{authorizeString}</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
Enter your GCP project ID, and run the command to create the image
|
||||||
|
in your project.
|
||||||
|
<TextInput
|
||||||
|
className='pf-v6-u-mt-sm pf-v6-u-mb-md'
|
||||||
|
value={customerProjectId}
|
||||||
|
type='text'
|
||||||
|
onChange={(_event, value) => setCustomerProjectId(value)}
|
||||||
|
aria-label='Project ID input'
|
||||||
|
placeholder='Project ID'
|
||||||
|
/>
|
||||||
|
<ClipboardCopy
|
||||||
|
isReadOnly
|
||||||
|
hoverTip='Copy'
|
||||||
|
clickTip='Copied'
|
||||||
|
variant={ClipboardCopyVariant.expansion}
|
||||||
|
>
|
||||||
|
{createImage}
|
||||||
|
</ClipboardCopy>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
Create an instance of your image by either accessing the{' '}
|
||||||
|
<Button
|
||||||
|
component='a'
|
||||||
|
target='_blank'
|
||||||
|
variant='link'
|
||||||
|
icon={<ExternalLinkAltIcon />}
|
||||||
|
iconPosition='right'
|
||||||
|
href={`https://console.cloud.google.com/compute/images`}
|
||||||
|
className='pf-v6-u-pl-0'
|
||||||
|
>
|
||||||
|
GCP console
|
||||||
|
</Button>{' '}
|
||||||
|
or by running the following command:
|
||||||
|
<ClipboardCopy
|
||||||
|
isReadOnly
|
||||||
|
hoverTip='Copy'
|
||||||
|
clickTip='Copied'
|
||||||
|
variant={ClipboardCopyVariant.expansion}
|
||||||
|
>
|
||||||
|
{createInstance}
|
||||||
|
</ClipboardCopy>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
139
src/Components/Launch/OciLaunchModal.tsx
Normal file
139
src/Components/Launch/OciLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
import React, { Fragment, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ClipboardCopy,
|
||||||
|
ClipboardCopyVariant,
|
||||||
|
List,
|
||||||
|
ListComponent,
|
||||||
|
ListItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalVariant,
|
||||||
|
OrderType,
|
||||||
|
Skeleton,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ComposesResponseItem,
|
||||||
|
useGetComposeStatusQuery,
|
||||||
|
} from '../../store/imageBuilderApi';
|
||||||
|
import { isOciUploadStatus } from '../../store/typeGuards';
|
||||||
|
import { resolveRelPath } from '../../Utilities/path';
|
||||||
|
|
||||||
|
type LaunchProps = {
|
||||||
|
isExpired: boolean;
|
||||||
|
compose: ComposesResponseItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OciLaunchModal = ({ isExpired, compose }: LaunchProps) => {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const { data, isSuccess, isFetching } = useGetComposeStatusQuery({
|
||||||
|
composeId: compose.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
if (!isSuccess) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = data?.image_status.upload_status?.options;
|
||||||
|
|
||||||
|
if (options && !isOciUploadStatus(options)) {
|
||||||
|
throw TypeError(
|
||||||
|
`Error: options must be of type OciUploadStatus, not ${typeof options}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
component='a'
|
||||||
|
target='_blank'
|
||||||
|
variant='link'
|
||||||
|
onClick={() => navigate(resolveRelPath(`imagewizard/${compose.id}`))}
|
||||||
|
isInline
|
||||||
|
>
|
||||||
|
Recreate image
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalToggle = () => {
|
||||||
|
setIsModalOpen(!isModalOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
isInline
|
||||||
|
isDisabled={data?.image_status.status !== 'success'}
|
||||||
|
onClick={handleModalToggle}
|
||||||
|
>
|
||||||
|
Image link
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleModalToggle}
|
||||||
|
variant={ModalVariant.large}
|
||||||
|
aria-label='Open launch guide modal'
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
title={'Launch with Oracle Cloud Infrastructure'}
|
||||||
|
labelId='modal-title'
|
||||||
|
description={compose.image_name}
|
||||||
|
/>
|
||||||
|
<ModalBody id='modal-box-body-basic'>
|
||||||
|
<List component={ListComponent.ol} type={OrderType.number}>
|
||||||
|
<ListItem>
|
||||||
|
Navigate to the{' '}
|
||||||
|
<Button
|
||||||
|
component='a'
|
||||||
|
target='_blank'
|
||||||
|
variant='link'
|
||||||
|
icon={<ExternalLinkAltIcon />}
|
||||||
|
iconPosition='right'
|
||||||
|
href={`https://cloud.oracle.com/compute/images`}
|
||||||
|
className='pf-v6-u-pl-0'
|
||||||
|
>
|
||||||
|
Oracle Cloud's Custom Images
|
||||||
|
</Button>{' '}
|
||||||
|
page.
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
Select{' '}
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>Import image</span>,
|
||||||
|
and enter the Object Storage URL of the image.
|
||||||
|
{!isFetching && (
|
||||||
|
<ClipboardCopy
|
||||||
|
isReadOnly
|
||||||
|
isExpanded
|
||||||
|
hoverTip='Copy'
|
||||||
|
clickTip='Copied'
|
||||||
|
variant={ClipboardCopyVariant.expansion}
|
||||||
|
>
|
||||||
|
{options?.url || ''}
|
||||||
|
</ClipboardCopy>
|
||||||
|
)}
|
||||||
|
{isFetching && <Skeleton />}
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
After the image is available, click on{' '}
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>Create instance</span>.
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
src/Components/Launch/useGenerateDefaultName.ts
Normal file
34
src/Components/Launch/useGenerateDefaultName.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
export const generateDefaultName = (imageName: string) => {
|
||||||
|
const date = new Date();
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const year = date.getFullYear().toString();
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
const dateTimeString = `${month}${day}${year}-${hours}${minutes}`;
|
||||||
|
|
||||||
|
// gcloud images are valid in the form of: (?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)
|
||||||
|
let newBlueprintName = imageName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9-]/g, '-')
|
||||||
|
.replace(/-{2,}/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
|
||||||
|
if (!/^[a-z]/.test(newBlueprintName)) {
|
||||||
|
newBlueprintName = 'i' + newBlueprintName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLength = 63;
|
||||||
|
const uniquePartLength = dateTimeString.length + 1;
|
||||||
|
const baseNameMaxLength = maxLength - uniquePartLength;
|
||||||
|
if (newBlueprintName.length > baseNameMaxLength) {
|
||||||
|
newBlueprintName = newBlueprintName.substring(0, baseNameMaxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (newBlueprintName.endsWith('-')) {
|
||||||
|
newBlueprintName = newBlueprintName.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${newBlueprintName}-${dateTimeString}`;
|
||||||
|
};
|
||||||
|
|
@ -156,7 +156,7 @@ const RegionsSelect = ({ composeId, handleClose }: RegionsSelectPropTypes) => {
|
||||||
<MenuToggle
|
<MenuToggle
|
||||||
variant='typeahead'
|
variant='typeahead'
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
innerRef={toggleRef}
|
ref={toggleRef}
|
||||||
isExpanded={isOpen}
|
isExpanded={isOpen}
|
||||||
>
|
>
|
||||||
<TextInputGroup isPlain>
|
<TextInputGroup isPlain>
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ 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';
|
||||||
|
|
|
||||||
24
src/Hooks/useGetUser.tsx
Normal file
24
src/Hooks/useGetUser.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
|
|
||||||
|
export const useGetUser = (auth: { getUser(): Promise<void | ChromeUser> }) => {
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
const [orgId, setOrgId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (!process.env.IS_ON_PREMISE) {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
const id = data?.identity.internal?.org_id;
|
||||||
|
setUserData(data);
|
||||||
|
setOrgId(id);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { userData, orgId };
|
||||||
|
};
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* - Please do NOT modify this file.
|
* - Please do NOT modify this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PACKAGE_VERSION = '2.10.4'
|
const PACKAGE_VERSION = '2.10.5'
|
||||||
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,6 +43,12 @@ 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,7 +44,8 @@ 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';
|
||||||
|
|
||||||
|
|
@ -89,6 +90,12 @@ 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;
|
||||||
|
|
@ -189,6 +196,12 @@ 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',
|
||||||
|
|
@ -376,6 +389,26 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
@ -627,6 +660,22 @@ 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']>,
|
||||||
|
|
@ -1230,6 +1279,10 @@ export const {
|
||||||
changeTimezone,
|
changeTimezone,
|
||||||
changeSatelliteRegistrationCommand,
|
changeSatelliteRegistrationCommand,
|
||||||
changeSatelliteCaCertificate,
|
changeSatelliteCaCertificate,
|
||||||
|
changeAapCallbackUrl,
|
||||||
|
changeAapHostConfigKey,
|
||||||
|
changeAapTlsCertificateAuthority,
|
||||||
|
changeAapTlsConfirmation,
|
||||||
addNtpServer,
|
addNtpServer,
|
||||||
removeNtpServer,
|
removeNtpServer,
|
||||||
changeHostname,
|
changeHostname,
|
||||||
|
|
|
||||||
|
|
@ -714,6 +714,9 @@ describe('Import modal', () => {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// AAP
|
||||||
|
await clickNext();
|
||||||
|
|
||||||
// Firstboot
|
// Firstboot
|
||||||
await clickNext();
|
await clickNext();
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ 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,4 +332,30 @@ 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,6 +513,123 @@ 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 First boot script', async () => {
|
test('clicking Next loads Ansible Automation Platform', async () => {
|
||||||
await renderCreateMode();
|
await renderCreateMode();
|
||||||
await goToServicesStep();
|
await goToServicesStep();
|
||||||
await clickNext();
|
await clickNext();
|
||||||
await screen.findByRole('heading', {
|
await screen.findByRole('heading', {
|
||||||
name: 'First boot configuration',
|
name: 'Ansible Automation Platform',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
14
src/test/fixtures/compliance.ts
vendored
14
src/test/fixtures/compliance.ts
vendored
|
|
@ -36,8 +36,20 @@ export const mockPolicies = {
|
||||||
profile_title: 'DISA STIG with GUI for Red Hat Enterprise Linux 8',
|
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: 3,
|
total: 4,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
16
src/test/fixtures/oscap.ts
vendored
16
src/test/fixtures/oscap.ts
vendored
|
|
@ -124,9 +124,17 @@ export const oscapCustomizationsPolicy = (
|
||||||
): GetOscapCustomizationsApiResponse => {
|
): 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
|
|
||||||
customizations.packages = customizations.packages!.filter(
|
// Simulate different levels of customization based on policy
|
||||||
(p) => p !== 'aide',
|
if (policy === 'custom-policy-123') {
|
||||||
);
|
// This policy has user-customized rules - only neovim remains
|
||||||
|
customizations.packages = ['neovim']; // User removed aide package
|
||||||
|
} else {
|
||||||
|
// Other policies: filter out a single package to simulate basic customizations
|
||||||
|
customizations.packages = customizations.packages!.filter(
|
||||||
|
(p) => p !== 'aide',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return customizations;
|
return customizations;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
58
src/test/fixtures/packages.ts
vendored
58
src/test/fixtures/packages.ts
vendored
|
|
@ -75,6 +75,64 @@ export const mockSourcesPackagesResults = (
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (search === 'sortingTest') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
package_name: 'alphaModule',
|
||||||
|
summary: 'Alpha module for sorting tests',
|
||||||
|
package_sources: [
|
||||||
|
{
|
||||||
|
name: 'alphaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '2.0',
|
||||||
|
end_date: '2025-12-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'alphaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '3.0',
|
||||||
|
end_date: '2027-12-01',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
package_name: 'betaModule',
|
||||||
|
summary: 'Beta module for sorting tests',
|
||||||
|
package_sources: [
|
||||||
|
{
|
||||||
|
name: 'betaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '2.0',
|
||||||
|
end_date: '2025-06-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'betaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '4.0',
|
||||||
|
end_date: '2028-06-01',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
package_name: 'gammaModule',
|
||||||
|
summary: 'Gamma module for sorting tests',
|
||||||
|
package_sources: [
|
||||||
|
{
|
||||||
|
name: 'gammaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '2.0',
|
||||||
|
end_date: '2025-08-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gammaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '1.5',
|
||||||
|
end_date: '2026-08-01',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
if (search === 'mock') {
|
if (search === 'mock') {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ 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