Compare commits
No commits in common. "main" and "v68" have entirely different histories.
321 changed files with 47785 additions and 15255 deletions
8
.eslintignore
Normal file
8
.eslintignore
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Ignore programatically generated API slices
|
||||||
|
imageBuilderApi.ts
|
||||||
|
contentSourcesApi.ts
|
||||||
|
rhsmApi.ts
|
||||||
|
provisioningApi.ts
|
||||||
|
edgeApi.ts
|
||||||
|
complianceApi.ts
|
||||||
|
composerCloudApi.ts
|
||||||
65
.eslintrc.yml
Normal file
65
.eslintrc.yml
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
extends: [
|
||||||
|
"plugin:jsx-a11y/recommended",
|
||||||
|
"@redhat-cloud-services/eslint-config-redhat-cloud-services",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react-redux/recommended"
|
||||||
|
]
|
||||||
|
globals:
|
||||||
|
insights: 'readonly'
|
||||||
|
shallow: readonly
|
||||||
|
render: 'readonly'
|
||||||
|
mount: 'readonly'
|
||||||
|
parser: "@typescript-eslint/parser"
|
||||||
|
parserOptions:
|
||||||
|
project: ["tsconfig.json"]
|
||||||
|
plugins:
|
||||||
|
- import
|
||||||
|
- disable-autofix
|
||||||
|
rules:
|
||||||
|
import/order:
|
||||||
|
- error
|
||||||
|
- groups:
|
||||||
|
- builtin
|
||||||
|
- external
|
||||||
|
- internal
|
||||||
|
- sibling
|
||||||
|
- parent
|
||||||
|
- index
|
||||||
|
alphabetize:
|
||||||
|
order: asc
|
||||||
|
caseInsensitive: true
|
||||||
|
newlines-between: always
|
||||||
|
pathGroups: # ensures the import of React is always on top
|
||||||
|
- pattern: react
|
||||||
|
group: builtin
|
||||||
|
position: before
|
||||||
|
pathGroupsExcludedImportTypes:
|
||||||
|
- react
|
||||||
|
prefer-const:
|
||||||
|
- error
|
||||||
|
- destructuring: any
|
||||||
|
no-console: error
|
||||||
|
eqeqeq: error
|
||||||
|
array-callback-return: warn
|
||||||
|
"@typescript-eslint/ban-ts-comment":
|
||||||
|
- error
|
||||||
|
- ts-expect-error: "allow-with-description"
|
||||||
|
ts-ignore: "allow-with-description"
|
||||||
|
ts-nocheck: true
|
||||||
|
ts-check: true
|
||||||
|
minimumDescriptionLength: 5
|
||||||
|
"@typescript-eslint/ban-types": off
|
||||||
|
disable-autofix/@typescript-eslint/no-unnecessary-condition: warn
|
||||||
|
# Temporarily disabled
|
||||||
|
jsx-a11y/no-autofocus: off
|
||||||
|
rulesdir/forbid-pf-relative-imports: off
|
||||||
|
overrides:
|
||||||
|
- files: ["src/tests/**/*.ts"]
|
||||||
|
extends: "plugin:testing-library/react"
|
||||||
|
- files: ["playwright/**/*.ts"]
|
||||||
|
extends: "plugin:playwright/recommended"
|
||||||
|
rules:
|
||||||
|
playwright/no-conditional-in-test: off
|
||||||
|
playwright/no-conditional-expect: off
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
1
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
---
|
|
||||||
name: Debian Image Builder Frontend CI/CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, develop]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_VERSION: "18"
|
|
||||||
DEBIAN_FRONTEND: noninteractive
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-test:
|
|
||||||
name: Build and Test Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build || echo "Build script not found"
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run test; then
|
|
||||||
npm test
|
|
||||||
else
|
|
||||||
echo "No test script found, skipping tests"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run linting
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run lint; then
|
|
||||||
npm run lint
|
|
||||||
else
|
|
||||||
echo "No lint script found, skipping linting"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: frontend-build
|
|
||||||
path: |
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
package:
|
|
||||||
name: Package Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
devscripts \
|
|
||||||
debhelper \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create debian directory
|
|
||||||
run: |
|
|
||||||
mkdir -p debian
|
|
||||||
cat > debian/control << EOF
|
|
||||||
Source: debian-image-builder-frontend
|
|
||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Debian Forge Team <team@debian-forge.org>
|
|
||||||
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
|
||||||
Standards-Version: 4.6.2
|
|
||||||
|
|
||||||
Package: debian-image-builder-frontend
|
|
||||||
Architecture: all
|
|
||||||
Depends: \${misc:Depends}, nodejs, nginx
|
|
||||||
Description: Debian Image Builder Frontend
|
|
||||||
Web-based frontend for Debian Image Builder with Cockpit integration.
|
|
||||||
Provides a user interface for managing image builds, blueprints,
|
|
||||||
and system configurations through a modern React application.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/rules << EOF
|
|
||||||
#!/usr/bin/make -f
|
|
||||||
%:
|
|
||||||
dh \$@
|
|
||||||
|
|
||||||
override_dh_auto_install:
|
|
||||||
dh_auto_install
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
|
||||||
|
|
||||||
# Copy built frontend files
|
|
||||||
if [ -d dist ]; then
|
|
||||||
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
elif [ -d build ]; then
|
|
||||||
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy source files for development
|
|
||||||
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
|
|
||||||
# Create nginx configuration
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /usr/share/debian-image-builder-frontend;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://localhost:8080/;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX_EOF
|
|
||||||
|
|
||||||
# Create cockpit manifest
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
|
||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"manifest": {
|
|
||||||
"name": "debian-image-builder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"title": "Debian Image Builder",
|
|
||||||
"description": "Build and manage Debian atomic images",
|
|
||||||
"url": "/usr/share/debian-image-builder-frontend",
|
|
||||||
"icon": "debian-logo",
|
|
||||||
"requires": {
|
|
||||||
"cockpit": ">= 200"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
COCKPIT_EOF
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/changelog << EOF
|
|
||||||
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Initial release
|
|
||||||
* Debian Image Builder Frontend with Cockpit integration
|
|
||||||
* React-based web interface for image management
|
|
||||||
|
|
||||||
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/compat << EOF
|
|
||||||
13
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x debian/rules
|
|
||||||
|
|
||||||
- name: Build Debian package
|
|
||||||
run: |
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
ls -la ../*.deb
|
|
||||||
|
|
||||||
- name: Upload Debian package
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: debian-image-builder-frontend-deb
|
|
||||||
path: ../*.deb
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
cockpit-integration:
|
|
||||||
name: Test Cockpit Integration
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Test cockpit integration
|
|
||||||
run: |
|
|
||||||
echo "Testing Cockpit integration..."
|
|
||||||
if [ -d cockpit ]; then
|
|
||||||
echo "Cockpit directory found:"
|
|
||||||
ls -la cockpit/
|
|
||||||
else
|
|
||||||
echo "No cockpit directory found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f package.json ]; then
|
|
||||||
echo "Package.json scripts:"
|
|
||||||
npm run
|
|
||||||
fi
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
---
|
|
||||||
name: Debian Image Builder Frontend CI/CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, develop]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_VERSION: "18"
|
|
||||||
DEBIAN_FRONTEND: noninteractive
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-test:
|
|
||||||
name: Build and Test Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build || echo "Build script not found"
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run test; then
|
|
||||||
npm test
|
|
||||||
else
|
|
||||||
echo "No test script found, skipping tests"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run linting
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run lint; then
|
|
||||||
npm run lint
|
|
||||||
else
|
|
||||||
echo "No lint script found, skipping linting"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: frontend-build
|
|
||||||
path: |
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
package:
|
|
||||||
name: Package Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
devscripts \
|
|
||||||
debhelper \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create debian directory
|
|
||||||
run: |
|
|
||||||
mkdir -p debian
|
|
||||||
cat > debian/control << EOF
|
|
||||||
Source: debian-image-builder-frontend
|
|
||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Debian Forge Team <team@debian-forge.org>
|
|
||||||
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
|
||||||
Standards-Version: 4.6.2
|
|
||||||
|
|
||||||
Package: debian-image-builder-frontend
|
|
||||||
Architecture: all
|
|
||||||
Depends: \${misc:Depends}, nodejs, nginx
|
|
||||||
Description: Debian Image Builder Frontend
|
|
||||||
Web-based frontend for Debian Image Builder with Cockpit integration.
|
|
||||||
Provides a user interface for managing image builds, blueprints,
|
|
||||||
and system configurations through a modern React application.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/rules << EOF
|
|
||||||
#!/usr/bin/make -f
|
|
||||||
%:
|
|
||||||
dh \$@
|
|
||||||
|
|
||||||
override_dh_auto_install:
|
|
||||||
dh_auto_install
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
|
||||||
|
|
||||||
# Copy built frontend files
|
|
||||||
if [ -d dist ]; then
|
|
||||||
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
elif [ -d build ]; then
|
|
||||||
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy source files for development
|
|
||||||
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
|
|
||||||
# Create nginx configuration
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /usr/share/debian-image-builder-frontend;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://localhost:8080/;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX_EOF
|
|
||||||
|
|
||||||
# Create cockpit manifest
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
|
||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"manifest": {
|
|
||||||
"name": "debian-image-builder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"title": "Debian Image Builder",
|
|
||||||
"description": "Build and manage Debian atomic images",
|
|
||||||
"url": "/usr/share/debian-image-builder-frontend",
|
|
||||||
"icon": "debian-logo",
|
|
||||||
"requires": {
|
|
||||||
"cockpit": ">= 200"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
COCKPIT_EOF
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/changelog << EOF
|
|
||||||
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Initial release
|
|
||||||
* Debian Image Builder Frontend with Cockpit integration
|
|
||||||
* React-based web interface for image management
|
|
||||||
|
|
||||||
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/compat << EOF
|
|
||||||
13
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x debian/rules
|
|
||||||
|
|
||||||
- name: Build Debian package
|
|
||||||
run: |
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
ls -la ../*.deb
|
|
||||||
|
|
||||||
- name: Upload Debian package
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: debian-image-builder-frontend-deb
|
|
||||||
path: ../*.deb
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
cockpit-integration:
|
|
||||||
name: Test Cockpit Integration
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Test cockpit integration
|
|
||||||
run: |
|
|
||||||
echo "Testing Cockpit integration..."
|
|
||||||
if [ -d cockpit ]; then
|
|
||||||
echo "Cockpit directory found:"
|
|
||||||
ls -la cockpit/
|
|
||||||
else
|
|
||||||
echo "No cockpit directory found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f package.json ]; then
|
|
||||||
echo "Package.json scripts:"
|
|
||||||
npm run
|
|
||||||
fi
|
|
||||||
84
.github/workflows/dev-checks.yml
vendored
84
.github/workflows/dev-checks.yml
vendored
|
|
@ -5,79 +5,35 @@ on:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
merge_group:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{github.workflow}}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
dev-check:
|
||||||
name: Build Check
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js 22
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'npm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Run build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
lint-checks:
|
|
||||||
name: ESLint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js 22
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
cache: 'npm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Run lint check
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
circular-dependencies:
|
|
||||||
name: Circular Dependencies Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js 22
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'npm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Check for circular dependencies
|
|
||||||
run: npm run circular
|
|
||||||
|
|
||||||
api-changes:
|
|
||||||
name: Manual API Changes Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js 22
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Check for manual changes to API
|
- name: Check for manual changes to API
|
||||||
run: |
|
run: npm run api:generate && [ -z "$(git status --porcelain=v1 2>/dev/null)" ] && echo "✓ No manual API changes." || echo "✗ API manually changed, please refer to the README for the procedure to follow for programmatically generated API endpoints." && [ -z "$(git status --porcelain=v1 2>/dev/null)" ]
|
||||||
npm run api
|
- name: Check for circular dependencies
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
run: npm run circular
|
||||||
echo
|
- name: Run build
|
||||||
echo "✗ API manually changed, please refer to the README for the procedure to follow for programmatically generated API endpoints."
|
run: npm run build
|
||||||
exit 1
|
- name: Run lint check
|
||||||
else
|
run: npm run lint
|
||||||
echo
|
- name: Run unit tests
|
||||||
echo "✓ No manual API changes."
|
run: npm run test:coverage
|
||||||
exit 0
|
- name: Run unit tests with cockpit
|
||||||
fi
|
run: npm run test:cockpit
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: ./coverage/junit.xml
|
||||||
|
verbose: true
|
||||||
|
|
|
||||||
9
.github/workflows/playwright.yml
vendored
9
.github/workflows/playwright.yml
vendored
|
|
@ -4,13 +4,6 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, reopened, synchronize, labeled, unlabeled]
|
types: [opened, reopened, synchronize, labeled, unlabeled]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
merge_group:
|
|
||||||
|
|
||||||
# this prevents multiple jobs from the same pr
|
|
||||||
# running when new changes are pushed.
|
|
||||||
concurrency:
|
|
||||||
group: ${{github.workflow}}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
playwright-tests:
|
playwright-tests:
|
||||||
|
|
@ -37,7 +30,7 @@ jobs:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install front-end dependencies
|
- name: Install front-end dependencies
|
||||||
|
|
|
||||||
1
.github/workflows/pr_best_practices.yml
vendored
1
.github/workflows/pr_best_practices.yml
vendored
|
|
@ -6,7 +6,6 @@ on:
|
||||||
types: [opened, synchronize, reopened, edited]
|
types: [opened, synchronize, reopened, edited]
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
merge_group:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pr-best-practices:
|
pr-best-practices:
|
||||||
|
|
|
||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -13,10 +13,10 @@ jobs:
|
||||||
# artefact name.
|
# artefact name.
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js 22
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
|
||||||
1
.github/workflows/stale-cleanup.yml
vendored
1
.github/workflows/stale-cleanup.yml
vendored
|
|
@ -8,7 +8,6 @@ jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
actions: write # needed to clean up the saved action state
|
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
51
.github/workflows/unit-tests.yml
vendored
51
.github/workflows/unit-tests.yml
vendored
|
|
@ -1,51 +0,0 @@
|
||||||
name: Unit Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
merge_group:
|
|
||||||
|
|
||||||
# this prevents multiple jobs from the same pr
|
|
||||||
# running when new changes are pushed.
|
|
||||||
concurrency:
|
|
||||||
group: ${{github.workflow}}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
unit-tests:
|
|
||||||
name: Service Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js 22
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'npm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Run unit tests
|
|
||||||
run: npm run test:coverage
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
files: ./coverage/junit.xml
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
cockpit-unit-tests:
|
|
||||||
name: Cockpit Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js 22
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'npm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Run unit tests with cockpit
|
|
||||||
run: npm run test:cockpit
|
|
||||||
51
.github/workflows/update-apis.yml
vendored
51
.github/workflows/update-apis.yml
vendored
|
|
@ -1,51 +0,0 @@
|
||||||
# This action checks API updates every day at 5:00 UTC.
|
|
||||||
name: Update API code generation
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 5 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-api:
|
|
||||||
name: "Update API definitions"
|
|
||||||
if: github.repository == 'osbuild/image-builder-frontend'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out code into the Go module directory
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Use Node.js 22
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Mark the working directory as safe for git
|
|
||||||
run: git config --global --add safe.directory "$(pwd)"
|
|
||||||
|
|
||||||
- name: Run API code generation
|
|
||||||
run: npm run api
|
|
||||||
|
|
||||||
- name: Check if there are any changes
|
|
||||||
run: |
|
|
||||||
if [ "$(git status --porcelain)" ]; then
|
|
||||||
echo
|
|
||||||
echo "API codegen is up-to-date"
|
|
||||||
exit "0"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
uses: peter-evans/create-pull-request@v6
|
|
||||||
with:
|
|
||||||
branch: update-apis
|
|
||||||
delete-branch: true
|
|
||||||
title: "api: regenerate api code generation"
|
|
||||||
commit-message: "api: regenerate api code generation"
|
|
||||||
body: Update api code generation
|
|
||||||
token: ${{ secrets.SCHUTZBOT_GITHUB_ACCESS_TOKEN }}
|
|
||||||
author: schutzbot <schutzbot@gmail.com>
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -48,4 +48,3 @@ rpmbuild
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
.env
|
.env
|
||||||
.auth
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ test:
|
||||||
- RUNNER:
|
- RUNNER:
|
||||||
- aws/fedora-41-x86_64
|
- aws/fedora-41-x86_64
|
||||||
- aws/fedora-42-x86_64
|
- aws/fedora-42-x86_64
|
||||||
- aws/rhel-10.1-nightly-x86_64
|
- aws/rhel-9.6-nightly-x86_64
|
||||||
|
- aws/rhel-10.0-nightly-x86_64
|
||||||
INTERNAL_NETWORK: ["true"]
|
INTERNAL_NETWORK: ["true"]
|
||||||
|
|
||||||
finish:
|
finish:
|
||||||
|
|
|
||||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"tsxSingleQuote": true,
|
||||||
|
"tsSingleQuote": true
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,9 @@ metadata:
|
||||||
build.appstudio.redhat.com/pull_request_number: '{{pull_request_number}}'
|
build.appstudio.redhat.com/pull_request_number: '{{pull_request_number}}'
|
||||||
build.appstudio.redhat.com/target_branch: '{{target_branch}}'
|
build.appstudio.redhat.com/target_branch: '{{target_branch}}'
|
||||||
pipelinesascode.tekton.dev/max-keep-runs: "3"
|
pipelinesascode.tekton.dev/max-keep-runs: "3"
|
||||||
pipelinesascode.tekton.dev/on-cel-expression: (event == "pull_request" && target_branch == "main") || (event == "push" && target_branch.startsWith("gh-readonly-queue/main/"))
|
pipelinesascode.tekton.dev/on-cel-expression: event == "pull_request" && target_branch
|
||||||
creationTimestamp:
|
== "main"
|
||||||
|
creationTimestamp: null
|
||||||
labels:
|
labels:
|
||||||
appstudio.openshift.io/application: insights-image-builder
|
appstudio.openshift.io/application: insights-image-builder
|
||||||
appstudio.openshift.io/component: image-builder-frontend
|
appstudio.openshift.io/component: image-builder-frontend
|
||||||
|
|
@ -45,7 +46,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: show-sbom
|
value: show-sbom
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:beb0616db051952b4b861dd8c3e00fa1c0eccbd926feddf71194d3bb3ace9ce7
|
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:002f7c8c1d2f9e09904035da414aba1188ae091df0ea9532cd997be05e73d594
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -64,7 +65,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: summary
|
value: summary
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:3f6e8513cbd70f0416eb6c6f2766973a754778526125ff33d8e3633def917091
|
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:76075b709fa06ed824cbc84f41448b397b85bfde1cf9809395ba6d286f5b7cbd
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -83,11 +84,13 @@ spec:
|
||||||
name: output-image
|
name: output-image
|
||||||
type: string
|
type: string
|
||||||
- default: .
|
- default: .
|
||||||
description: Path to the source code of an application's component from where to build image.
|
description: Path to the source code of an application's component from where
|
||||||
|
to build image.
|
||||||
name: path-context
|
name: path-context
|
||||||
type: string
|
type: string
|
||||||
- default: Dockerfile
|
- default: Dockerfile
|
||||||
description: Path to the Dockerfile inside the context specified by parameter path-context
|
description: Path to the Dockerfile inside the context specified by parameter
|
||||||
|
path-context
|
||||||
name: dockerfile
|
name: dockerfile
|
||||||
type: string
|
type: string
|
||||||
- default: "false"
|
- default: "false"
|
||||||
|
|
@ -107,7 +110,8 @@ spec:
|
||||||
name: prefetch-input
|
name: prefetch-input
|
||||||
type: string
|
type: string
|
||||||
- default: ""
|
- default: ""
|
||||||
description: Image tag expiration time, time values could be something like 1h, 2d, 3w for hours, days, and weeks, respectively.
|
description: Image tag expiration time, time values could be something like
|
||||||
|
1h, 2d, 3w for hours, days, and weeks, respectively.
|
||||||
name: image-expires-after
|
name: image-expires-after
|
||||||
- default: "false"
|
- default: "false"
|
||||||
description: Build a source image.
|
description: Build a source image.
|
||||||
|
|
@ -152,7 +156,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: init
|
value: init
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:08e18a4dc5f947c1d20e8353a19d013144bea87b72f67236b165dd4778523951
|
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:7a24924417260b7094541caaedd2853dc8da08d4bb0968f710a400d3e8062063
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -169,7 +173,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: git-clone
|
value: git-clone
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:7939000e2f92fc8b5d2c4ee4ba9000433c5aa7700d2915a1d4763853d5fd1fd4
|
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:3ced9a6b9d8520773d3ffbf062190515a362ecda11e72f56e38e4dd980294b57
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -194,7 +198,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: prefetch-dependencies
|
value: prefetch-dependencies
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:08eec5362aa774347f08a210531a6901020778a08ca921b02758a91b5b2e1357
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -238,7 +242,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: buildah
|
value: buildah
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:c777fdb0947aff3e4ac29a93ed6358c6f7994e6b150154427646788ec773c440
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -270,7 +274,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: build-image-index
|
value: build-image-index
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:72f77a8c62f9d6f69ab5c35170839e4b190026e6cc3d7d4ceafa7033fc30ad7b
|
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:1b357f2ed430d18a009740a1783dd15af70ce1e23dc6254da1a83e9ec595d5be
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -282,9 +286,7 @@ spec:
|
||||||
- name: build-source-image
|
- name: build-source-image
|
||||||
params:
|
params:
|
||||||
- name: BINARY_IMAGE
|
- name: BINARY_IMAGE
|
||||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
value: $(params.output-image)
|
||||||
- name: BINARY_IMAGE_DIGEST
|
|
||||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
|
||||||
runAfter:
|
runAfter:
|
||||||
- build-image-index
|
- build-image-index
|
||||||
taskRef:
|
taskRef:
|
||||||
|
|
@ -292,7 +294,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: source-build
|
value: source-build
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.3@sha256:96ed9431854ecf9805407dca77b063abdf7aba1b3b9d1925a5c6145c6b7e95fd
|
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.2@sha256:11029fa30652154f772b44132f8a116382c136a6223e8f9576137f99b9901dcb
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -321,7 +323,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: sast-shell-check
|
value: sast-shell-check
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:4a63982791a1a68f560c486f524ef5b9fdbeee0c16fe079eee3181a2cfd1c1bf
|
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:188a4f6a582ac43d4de46c3998ded3c2a8ee237fb0604d90559a3b6e0aa62b0f
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -337,8 +339,6 @@ spec:
|
||||||
params:
|
params:
|
||||||
- name: image-url
|
- name: image-url
|
||||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||||
- name: image-digest
|
|
||||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
|
||||||
runAfter:
|
runAfter:
|
||||||
- build-image-index
|
- build-image-index
|
||||||
taskRef:
|
taskRef:
|
||||||
|
|
@ -346,7 +346,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: sast-unicode-check
|
value: sast-unicode-check
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check:0.3@sha256:bec18fa5e82e801c3f267f29bf94535a5024e72476f2b27cca7271d506abb5ad
|
value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check:0.2@sha256:e4a5215b45b1886a185a9db8ab392f8440c2b0848f76d719885637cf8d2628ed
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -371,7 +371,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: deprecated-image-check
|
value: deprecated-image-check
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:1d07d16810c26713f3d875083924d93697900147364360587ccb5a63f2c31012
|
value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:ecd33669676b3a193ff4c2c6223cb912cc1b0cf5cc36e080eaec7718500272cf
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -393,7 +393,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: clair-scan
|
value: clair-scan
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:893ffa3ce26b061e21bb4d8db9ef7ed4ddd4044fe7aa5451ef391034da3ff759
|
value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:878ae247ffc58d95a9ac68e4d658ef91ef039363e03e65a386bc0ead02d9d7d8
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -413,7 +413,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: ecosystem-cert-preflight-checks
|
value: ecosystem-cert-preflight-checks
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:302828e9d7abc72b8a44fb2b9be068f86c982d8e5f4550b8bf654571d6361ee8
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -435,7 +435,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: sast-snyk-check
|
value: sast-snyk-check
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:351f2dce893159b703e9b6d430a2450b3df9967cb9bd3adb46852df8ccfe4c0d
|
value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:15a945b054b245f6713845dd6ae813d373c9f9cbac386d7382964f1b70ae3076
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -460,7 +460,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: clamav-scan
|
value: clamav-scan
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:cce2dfcc5bd6e91ee54aacdadad523b013eeae5cdaa7f6a4624b8cbcc040f439
|
value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.2@sha256:98d94290d6f21b6e231485326e3629bbcdec75c737b84e05ac9eac78f9a2c8b4
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -471,10 +471,8 @@ spec:
|
||||||
- "false"
|
- "false"
|
||||||
- name: apply-tags
|
- name: apply-tags
|
||||||
params:
|
params:
|
||||||
- name: IMAGE_URL
|
- name: IMAGE
|
||||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||||
- name: IMAGE_DIGEST
|
|
||||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
|
||||||
runAfter:
|
runAfter:
|
||||||
- build-image-index
|
- build-image-index
|
||||||
taskRef:
|
taskRef:
|
||||||
|
|
@ -482,7 +480,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: apply-tags
|
value: apply-tags
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.2@sha256:70881c97a4c51ee1f4d023fa1110e0bdfcfd2f51d9a261fa543c3862b9a4eee9
|
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.1@sha256:9d9871143ab3a818f681488be6074f5b2f892c1843795a46f6daf3f5487e72d1
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -503,7 +501,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: push-dockerfile
|
value: push-dockerfile
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:c82189e5d331e489cff99f0399f133fd3fad08921bea86747dfa379d1b5c748d
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -523,7 +521,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: rpms-signature-scan
|
value: rpms-signature-scan
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:1b6c20ab3dbfb0972803d3ebcb2fa72642e59400c77bd66dfd82028bdd09e120
|
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:297c2d8928aa3b114fcb1ba5d9da8b10226b68fed30706e78a6a5089c6cd30e3
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -544,7 +542,7 @@ spec:
|
||||||
- name: workspace
|
- name: workspace
|
||||||
volumeClaimTemplate:
|
volumeClaimTemplate:
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp:
|
creationTimestamp: null
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteOnce
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ metadata:
|
||||||
build.appstudio.redhat.com/commit_sha: '{{revision}}'
|
build.appstudio.redhat.com/commit_sha: '{{revision}}'
|
||||||
build.appstudio.redhat.com/target_branch: '{{target_branch}}'
|
build.appstudio.redhat.com/target_branch: '{{target_branch}}'
|
||||||
pipelinesascode.tekton.dev/max-keep-runs: "3"
|
pipelinesascode.tekton.dev/max-keep-runs: "3"
|
||||||
pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch == "main"
|
pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch
|
||||||
creationTimestamp:
|
== "main"
|
||||||
|
creationTimestamp: null
|
||||||
labels:
|
labels:
|
||||||
appstudio.openshift.io/application: insights-image-builder
|
appstudio.openshift.io/application: insights-image-builder
|
||||||
appstudio.openshift.io/component: image-builder-frontend
|
appstudio.openshift.io/component: image-builder-frontend
|
||||||
|
|
@ -42,7 +43,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: show-sbom
|
value: show-sbom
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:beb0616db051952b4b861dd8c3e00fa1c0eccbd926feddf71194d3bb3ace9ce7
|
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:002f7c8c1d2f9e09904035da414aba1188ae091df0ea9532cd997be05e73d594
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -61,7 +62,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: summary
|
value: summary
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:3f6e8513cbd70f0416eb6c6f2766973a754778526125ff33d8e3633def917091
|
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:76075b709fa06ed824cbc84f41448b397b85bfde1cf9809395ba6d286f5b7cbd
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -80,11 +81,13 @@ spec:
|
||||||
name: output-image
|
name: output-image
|
||||||
type: string
|
type: string
|
||||||
- default: .
|
- default: .
|
||||||
description: Path to the source code of an application's component from where to build image.
|
description: Path to the source code of an application's component from where
|
||||||
|
to build image.
|
||||||
name: path-context
|
name: path-context
|
||||||
type: string
|
type: string
|
||||||
- default: Dockerfile
|
- default: Dockerfile
|
||||||
description: Path to the Dockerfile inside the context specified by parameter path-context
|
description: Path to the Dockerfile inside the context specified by parameter
|
||||||
|
path-context
|
||||||
name: dockerfile
|
name: dockerfile
|
||||||
type: string
|
type: string
|
||||||
- default: "false"
|
- default: "false"
|
||||||
|
|
@ -104,7 +107,8 @@ spec:
|
||||||
name: prefetch-input
|
name: prefetch-input
|
||||||
type: string
|
type: string
|
||||||
- default: ""
|
- default: ""
|
||||||
description: Image tag expiration time, time values could be something like 1h, 2d, 3w for hours, days, and weeks, respectively.
|
description: Image tag expiration time, time values could be something like
|
||||||
|
1h, 2d, 3w for hours, days, and weeks, respectively.
|
||||||
name: image-expires-after
|
name: image-expires-after
|
||||||
- default: "false"
|
- default: "false"
|
||||||
description: Build a source image.
|
description: Build a source image.
|
||||||
|
|
@ -149,7 +153,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: init
|
value: init
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:08e18a4dc5f947c1d20e8353a19d013144bea87b72f67236b165dd4778523951
|
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:7a24924417260b7094541caaedd2853dc8da08d4bb0968f710a400d3e8062063
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -166,7 +170,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: git-clone
|
value: git-clone
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:7939000e2f92fc8b5d2c4ee4ba9000433c5aa7700d2915a1d4763853d5fd1fd4
|
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:3ced9a6b9d8520773d3ffbf062190515a362ecda11e72f56e38e4dd980294b57
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -191,7 +195,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: prefetch-dependencies
|
value: prefetch-dependencies
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:08eec5362aa774347f08a210531a6901020778a08ca921b02758a91b5b2e1357
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -235,7 +239,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: buildah
|
value: buildah
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:c777fdb0947aff3e4ac29a93ed6358c6f7994e6b150154427646788ec773c440
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -267,7 +271,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: build-image-index
|
value: build-image-index
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:72f77a8c62f9d6f69ab5c35170839e4b190026e6cc3d7d4ceafa7033fc30ad7b
|
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:1b357f2ed430d18a009740a1783dd15af70ce1e23dc6254da1a83e9ec595d5be
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -279,9 +283,7 @@ spec:
|
||||||
- name: build-source-image
|
- name: build-source-image
|
||||||
params:
|
params:
|
||||||
- name: BINARY_IMAGE
|
- name: BINARY_IMAGE
|
||||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
value: $(params.output-image)
|
||||||
- name: BINARY_IMAGE_DIGEST
|
|
||||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
|
||||||
runAfter:
|
runAfter:
|
||||||
- build-image-index
|
- build-image-index
|
||||||
taskRef:
|
taskRef:
|
||||||
|
|
@ -289,7 +291,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: source-build
|
value: source-build
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.3@sha256:96ed9431854ecf9805407dca77b063abdf7aba1b3b9d1925a5c6145c6b7e95fd
|
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.2@sha256:11029fa30652154f772b44132f8a116382c136a6223e8f9576137f99b9901dcb
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -318,7 +320,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: sast-shell-check
|
value: sast-shell-check
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:4a63982791a1a68f560c486f524ef5b9fdbeee0c16fe079eee3181a2cfd1c1bf
|
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:188a4f6a582ac43d4de46c3998ded3c2a8ee237fb0604d90559a3b6e0aa62b0f
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -334,8 +336,6 @@ spec:
|
||||||
params:
|
params:
|
||||||
- name: image-url
|
- name: image-url
|
||||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||||
- name: image-digest
|
|
||||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
|
||||||
runAfter:
|
runAfter:
|
||||||
- build-image-index
|
- build-image-index
|
||||||
taskRef:
|
taskRef:
|
||||||
|
|
@ -343,7 +343,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: sast-unicode-check
|
value: sast-unicode-check
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check:0.3@sha256:bec18fa5e82e801c3f267f29bf94535a5024e72476f2b27cca7271d506abb5ad
|
value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check:0.2@sha256:e4a5215b45b1886a185a9db8ab392f8440c2b0848f76d719885637cf8d2628ed
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -368,7 +368,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: deprecated-image-check
|
value: deprecated-image-check
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:1d07d16810c26713f3d875083924d93697900147364360587ccb5a63f2c31012
|
value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:ecd33669676b3a193ff4c2c6223cb912cc1b0cf5cc36e080eaec7718500272cf
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -390,7 +390,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: clair-scan
|
value: clair-scan
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:893ffa3ce26b061e21bb4d8db9ef7ed4ddd4044fe7aa5451ef391034da3ff759
|
value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:878ae247ffc58d95a9ac68e4d658ef91ef039363e03e65a386bc0ead02d9d7d8
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -410,7 +410,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: ecosystem-cert-preflight-checks
|
value: ecosystem-cert-preflight-checks
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:302828e9d7abc72b8a44fb2b9be068f86c982d8e5f4550b8bf654571d6361ee8
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -432,7 +432,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: sast-snyk-check
|
value: sast-snyk-check
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:351f2dce893159b703e9b6d430a2450b3df9967cb9bd3adb46852df8ccfe4c0d
|
value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:15a945b054b245f6713845dd6ae813d373c9f9cbac386d7382964f1b70ae3076
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -457,7 +457,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: clamav-scan
|
value: clamav-scan
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:cce2dfcc5bd6e91ee54aacdadad523b013eeae5cdaa7f6a4624b8cbcc040f439
|
value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.2@sha256:98d94290d6f21b6e231485326e3629bbcdec75c737b84e05ac9eac78f9a2c8b4
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -468,10 +468,8 @@ spec:
|
||||||
- "false"
|
- "false"
|
||||||
- name: apply-tags
|
- name: apply-tags
|
||||||
params:
|
params:
|
||||||
- name: IMAGE_URL
|
- name: IMAGE
|
||||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||||
- name: IMAGE_DIGEST
|
|
||||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
|
||||||
runAfter:
|
runAfter:
|
||||||
- build-image-index
|
- build-image-index
|
||||||
taskRef:
|
taskRef:
|
||||||
|
|
@ -479,7 +477,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: apply-tags
|
value: apply-tags
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.2@sha256:70881c97a4c51ee1f4d023fa1110e0bdfcfd2f51d9a261fa543c3862b9a4eee9
|
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.1@sha256:9d9871143ab3a818f681488be6074f5b2f892c1843795a46f6daf3f5487e72d1
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -500,7 +498,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: push-dockerfile
|
value: push-dockerfile
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:c82189e5d331e489cff99f0399f133fd3fad08921bea86747dfa379d1b5c748d
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -520,7 +518,7 @@ spec:
|
||||||
- name: name
|
- name: name
|
||||||
value: rpms-signature-scan
|
value: rpms-signature-scan
|
||||||
- name: bundle
|
- name: bundle
|
||||||
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:1b6c20ab3dbfb0972803d3ebcb2fa72642e59400c77bd66dfd82028bdd09e120
|
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:297c2d8928aa3b114fcb1ba5d9da8b10226b68fed30706e78a6a5089c6cd30e3
|
||||||
- name: kind
|
- name: kind
|
||||||
value: task
|
value: task
|
||||||
resolver: bundles
|
resolver: bundles
|
||||||
|
|
@ -541,7 +539,7 @@ spec:
|
||||||
- name: workspace
|
- name: workspace
|
||||||
volumeClaimTemplate:
|
volumeClaimTemplate:
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp:
|
creationTimestamp: null
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteOnce
|
||||||
|
|
|
||||||
6
Makefile
6
Makefile
|
|
@ -1,13 +1,12 @@
|
||||||
PACKAGE_NAME = cockpit-image-builder
|
PACKAGE_NAME = cockpit-image-builder
|
||||||
INSTALL_DIR_BASE = /share/cockpit/
|
INSTALL_DIR = /share/cockpit/$(PACKAGE_NAME)
|
||||||
INSTALL_DIR = $(INSTALL_DIR_BASE)$(PACKAGE_NAME)
|
|
||||||
APPSTREAMFILE=org.image-builder.$(PACKAGE_NAME).metainfo.xml
|
APPSTREAMFILE=org.image-builder.$(PACKAGE_NAME).metainfo.xml
|
||||||
|
|
||||||
VERSION := $(shell (cd "$(SRCDIR)" && grep "^Version:" cockpit/$(PACKAGE_NAME).spec | sed 's/[^[:digit:]]*\([[:digit:]]\+\).*/\1/'))
|
VERSION := $(shell (cd "$(SRCDIR)" && grep "^Version:" cockpit/$(PACKAGE_NAME).spec | sed 's/[^[:digit:]]*\([[:digit:]]\+\).*/\1/'))
|
||||||
COMMIT = $(shell (cd "$(SRCDIR)" && git rev-parse HEAD))
|
COMMIT = $(shell (cd "$(SRCDIR)" && git rev-parse HEAD))
|
||||||
|
|
||||||
# TODO: figure out a strategy for keeping this updated
|
# TODO: figure out a strategy for keeping this updated
|
||||||
COCKPIT_REPO_COMMIT = a70142a7a6f9c4e78e71f3c4ec738b6db2fbb04f
|
COCKPIT_REPO_COMMIT = b0e82161b4afcb9f0a6fddd8ff94380e983b2238
|
||||||
COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
|
COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
|
||||||
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
|
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
|
||||||
|
|
||||||
|
|
@ -56,7 +55,6 @@ cockpit/devel-uninstall:
|
||||||
cockpit/devel-install: PREFIX=~/.local
|
cockpit/devel-install: PREFIX=~/.local
|
||||||
cockpit/devel-install:
|
cockpit/devel-install:
|
||||||
PREFIX="~/.local"
|
PREFIX="~/.local"
|
||||||
mkdir -p $(PREFIX)$(INSTALL_DIR_BASE)
|
|
||||||
ln -s $(shell pwd)/cockpit/public $(PREFIX)$(INSTALL_DIR)
|
ln -s $(shell pwd)/cockpit/public $(PREFIX)$(INSTALL_DIR)
|
||||||
|
|
||||||
.PHONY: cockpit/download
|
.PHONY: cockpit/download
|
||||||
|
|
|
||||||
211
README.md
211
README.md
|
|
@ -19,20 +19,16 @@ Frontend code for Image Builder.
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
1. [How to build and run image-builder-frontend](#frontend-development)
|
1. [How to build and run image-builder-frontend](#frontend-development)
|
||||||
1. [Frontend Development](#frontend-development)
|
1. [Frontend Development](#frontend-development)
|
||||||
2. [Image builder as Cockpit plugin](#image-builder-as-cockpit-plugin)
|
1. [API](#api-endpoints)
|
||||||
3. [Backend Development](#backend-development)
|
2. [Unleash feature flags](#unleash-feature-flags)
|
||||||
2. [API](#api-endpoints)
|
2. [Backend Development](#backend-development)
|
||||||
3. [Unleash feature flags](#unleash-feature-flags)
|
2. [File structure](#file-structure)
|
||||||
4. [File structure](#file-structure)
|
3. [Style Guidelines](#style-guidelines)
|
||||||
5. [Style Guidelines](#style-guidelines)
|
4. [Test Guidelines](#test-guidelines)
|
||||||
6. [Test Guidelines](#test-guidelines)
|
5. [Running hosted service Playwright tests](#running-hosted-service-playwright-tests)
|
||||||
7. [Running hosted service Playwright tests](#running-hosted-service-playwright-tests)
|
|
||||||
|
|
||||||
## How to build and run image-builder-frontend
|
## How to build and run image-builder-frontend
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Running image-builder-frontend against [console.redhat.com](https://console.redhat.com/) requires connection to the Red Hat VPN, which is only available to Red Hat employees. External contributors can locally run [image builder as Cockpit plugin](#image-builder-as-cockpit-plugin).
|
|
||||||
|
|
||||||
### Frontend Development
|
### Frontend Development
|
||||||
|
|
||||||
To develop the frontend you can use a proxy to run image-builder-frontend locally
|
To develop the frontend you can use a proxy to run image-builder-frontend locally
|
||||||
|
|
@ -43,7 +39,7 @@ worrying if a feature from stage has been released yet.
|
||||||
|
|
||||||
#### Nodejs and npm version
|
#### Nodejs and npm version
|
||||||
|
|
||||||
Make sure you have npm@10 and node 22+ installed. If you need multiple versions of nodejs check out [nvm](https://github.com/nvm-sh/nvm).
|
Make sure you have npm@10 and node 18+ installed. If you need multiple versions of nodejs check out [nvm](https://github.com/nvm-sh/nvm).
|
||||||
|
|
||||||
#### Webpack proxy
|
#### Webpack proxy
|
||||||
|
|
||||||
|
|
@ -73,70 +69,52 @@ echo "127.0.0.1 stage.foo.redhat.com" >> /etc/hosts
|
||||||
|
|
||||||
4. open browser at `https://stage.foo.redhat.com:1337/beta/insights/image-builder`
|
4. open browser at `https://stage.foo.redhat.com:1337/beta/insights/image-builder`
|
||||||
|
|
||||||
### Image builder as Cockpit plugin
|
#### Insights proxy (deprecated)
|
||||||
|
|
||||||
> [!NOTE]
|
1. Clone the insights proxy: https://github.com/RedHatInsights/insights-proxy
|
||||||
> Issues marked with [cockpit-image-builder](https://github.com/osbuild/image-builder-frontend/issues?q=is%3Aissue%20state%3Aopen%20label%3Acockpit-image-builder) label are reproducible in image builder plugin and can be worked on by external contributors without connection to the Red Hat VPN.
|
|
||||||
|
|
||||||
#### Cockpit setup
|
2. Setting up the proxy
|
||||||
To install and setup Cockpit follow guide at: https://cockpit-project.org/running.html
|
|
||||||
|
|
||||||
#### On-premises image builder installation and configuration
|
Choose a runner (podman or docker), and point the SPANDX_CONFIG variable to
|
||||||
To install and configure `osbuild-composer` on your local machine follow our documentation: https://osbuild.org/docs/on-premises/installation/
|
`profile/local-frontend.js` included in image-builder-frontend.
|
||||||
|
|
||||||
#### Scripts for local development of image builder plugin
|
```bash
|
||||||
|
sudo insights-proxy/scripts/patch-etc-hosts.sh
|
||||||
|
export RUNNER="podman"
|
||||||
|
export SPANDX_CONFIG=$PATH_TO/image-builder-frontend/profiles/local-frontend.js
|
||||||
|
sudo -E insights-proxy/scripts/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
The following scripts are used to build the frontend with Webpack and install it into the Cockpit directories. These scripts streamline the development process by automating build and installation steps.
|
3. Starting up image-builder-frontend
|
||||||
|
|
||||||
Runs Webpack with the specified configuration (cockpit/webpack.config.ts) to build the frontend assets.
|
In the image-builder-frontend checkout directory
|
||||||
Use this command whenever you need to compile the latest changes in your frontend code.
|
|
||||||
|
|
||||||
Creates the necessary directory in the user's local Cockpit share (~/.local/share/cockpit/).
|
```bash
|
||||||
Creates a symbolic link (image-builder-frontend) pointing to the built frontend assets (cockpit/public).
|
npm install
|
||||||
Use this command after building the frontend to install it locally for development purposes.
|
npm start
|
||||||
The symbolic link allows Cockpit to serve the frontend assets from your local development environment,
|
```
|
||||||
making it easier to test changes in real-time without deploying to a remote server.
|
|
||||||
|
|
||||||
```bash
|
The UI should be running on
|
||||||
make cockpit/build
|
https://prod.foo.redhat.com:1337/beta/insights/image-builder/landing.
|
||||||
```
|
Note that this requires you to have access to either production or stage (plus VPN and proxy config) of insights.
|
||||||
|
|
||||||
```bash
|
#### API endpoints
|
||||||
make cockpit/devel-install
|
|
||||||
```
|
|
||||||
|
|
||||||
To uninstall and remove the symbolic link, run the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make cockpit/devel-uninstall
|
|
||||||
```
|
|
||||||
|
|
||||||
For convenience, you can run the following to combine all three steps:
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make cockpit/devel
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend Development
|
|
||||||
|
|
||||||
To develop both the frontend and the backend you can again use the proxy to run both the
|
|
||||||
frontend and backend locally against the chrome at cloud.redhat.com. For instructions
|
|
||||||
see the [osbuild-getting-started project](https://github.com/osbuild/osbuild-getting-started).
|
|
||||||
|
|
||||||
## API endpoints
|
|
||||||
|
|
||||||
API slice definitions are programmatically generated using the [@rtk-query/codegen-openapi](https://redux-toolkit.js.org/rtk-query/usage/code-generation) package.
|
API slice definitions are programmatically generated using the [@rtk-query/codegen-openapi](https://redux-toolkit.js.org/rtk-query/usage/code-generation) package.
|
||||||
|
|
||||||
The OpenAPI schema are imported during code generation. OpenAPI configuration files are
|
OpenAPI schema for the endpoints are stored in `/api/schema`. Their
|
||||||
stored in `/api/config`. Each endpoint has a corresponding empty API slice and generated API
|
corresponding configuration files are stored in `/api/config`. Each endpoint
|
||||||
slice which are stored in `/src/store`.
|
has a corresponding empty API slice and generated API slice which are stored in
|
||||||
|
`/src/store`.
|
||||||
|
|
||||||
### Add a new API schema
|
##### Add a new API
|
||||||
|
|
||||||
For a hypothetical API called foobar
|
For a hypothetical API called foobar
|
||||||
|
|
||||||
1. Create a new "empty" API file under `src/store/emptyFoobarApi.ts` that has following
|
1. Download the foobar API OpenAPI json or yaml representation under
|
||||||
|
`api/schema/foobar.json`
|
||||||
|
|
||||||
|
2. Create a new "empty" API file under `src/store/emptyFoobarApi.ts` that has following
|
||||||
content:
|
content:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|
@ -152,21 +130,21 @@ export const emptyFoobarApi = createApi({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Declare new constant `FOOBAR_API` with the API url in `src/constants.ts`
|
3. Declare new constant `FOOBAR_API` with the API url in `src/constants.ts`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const FOOBAR_API = 'api/foobar/v1'
|
export const FOOBAR_API = 'api/foobar/v1'
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create the config file for code generation in `api/config/foobar.ts` containing:
|
4. Create the config file for code generation in `api/config/foobar.ts` containing:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||||
|
|
||||||
const config: ConfigFile = {
|
const config: ConfigFile = {
|
||||||
schemaFile: 'URL_TO_THE_OPENAPI_SCHEMA',
|
schemaFile: '../schema/foobar.json',
|
||||||
apiFile: '../../src/store/emptyFoobarApi.ts',
|
apiFile: '../../src/store/emptyFoobarApi.ts',
|
||||||
apiImport: 'emptyContentSourcesApi',
|
apiImport: 'emptyEdgeApi',
|
||||||
outputFile: '../../src/store/foobarApi.ts',
|
outputFile: '../../src/store/foobarApi.ts',
|
||||||
exportName: 'foobarApi',
|
exportName: 'foobarApi',
|
||||||
hooks: true,
|
hooks: true,
|
||||||
|
|
@ -174,16 +152,20 @@ const config: ConfigFile = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Update the `eslint.config.js` file by adding the generated code path to the ignores array:
|
5. Update the `api.sh` script by adding a new line for npx to generate the code:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
ignores: [
|
npx @rtk-query/codegen-openapi ./api/config/foobar.ts &
|
||||||
<other ignored files>,
|
|
||||||
'**/foobarApi.ts',
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
5. run api generation
|
|
||||||
|
6. Update the `.eslintignore` file by adding a new line for the generated code:
|
||||||
|
|
||||||
|
```
|
||||||
|
foobarApi.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
7. run api generation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run api
|
npm run api
|
||||||
|
|
@ -191,12 +173,12 @@ npm run api
|
||||||
|
|
||||||
And voilà!
|
And voilà!
|
||||||
|
|
||||||
### Add a new endpoint
|
##### Add a new endpoint
|
||||||
|
|
||||||
To add a new endpoint, simply update the `api/config/foobar.ts` file with new
|
To add a new endpoint, simply update the `api/config/foobar.ts` file with new
|
||||||
endpoints in the `filterEndpoints` table.
|
endpoints in the `filterEndpoints` table.
|
||||||
|
|
||||||
## Unleash feature flags
|
#### Unleash feature flags
|
||||||
|
|
||||||
Your user needs to have the corresponding rights, do the
|
Your user needs to have the corresponding rights, do the
|
||||||
same as this MR in internal gitlab https://gitlab.cee.redhat.com/service/app-interface/-/merge_requests/79225
|
same as this MR in internal gitlab https://gitlab.cee.redhat.com/service/app-interface/-/merge_requests/79225
|
||||||
|
|
@ -212,7 +194,7 @@ existing flags:
|
||||||
|
|
||||||
https://github.com/RedHatInsights/image-builder-frontend/blob/c84b493eba82ce83a7844943943d91112ffe8322/src/Components/ImagesTable/ImageLink.js#L99
|
https://github.com/RedHatInsights/image-builder-frontend/blob/c84b493eba82ce83a7844943943d91112ffe8322/src/Components/ImagesTable/ImageLink.js#L99
|
||||||
|
|
||||||
### Mocking flags for tests
|
##### Mocking flags for tests
|
||||||
|
|
||||||
Flags can be mocked for the unit tests to access some feature. Checkout:
|
Flags can be mocked for the unit tests to access some feature. Checkout:
|
||||||
https://github.com/osbuild/image-builder-frontend/blob/9a464e416bc3769cfc8e23b62f1dd410eb0e0455/src/test/Components/CreateImageWizard/CreateImageWizard.test.tsx#L49
|
https://github.com/osbuild/image-builder-frontend/blob/9a464e416bc3769cfc8e23b62f1dd410eb0e0455/src/test/Components/CreateImageWizard/CreateImageWizard.test.tsx#L49
|
||||||
|
|
@ -222,18 +204,66 @@ base, then it's good practice to test the two of them. If not, only test what's
|
||||||
actually owned by the frontend project.
|
actually owned by the frontend project.
|
||||||
|
|
||||||
|
|
||||||
### Cleaning the flags
|
##### Cleaning the flags
|
||||||
|
|
||||||
Unleash toggles are expected to live for a limited amount of time, documentation
|
Unleash toggles are expected to live for a limited amount of time, documentation
|
||||||
specify 40 days for a release, we should keep that in mind for each toggle
|
specify 40 days for a release, we should keep that in mind for each toggle
|
||||||
we're planning on using.
|
we're planning on using.
|
||||||
|
|
||||||
|
### Backend Development
|
||||||
|
|
||||||
|
To develop both the frontend and the backend you can again use the proxy to run both the
|
||||||
|
frontend and backend locally against the chrome at cloud.redhat.com. For instructions
|
||||||
|
see the [osbuild-getting-started project](https://github.com/osbuild/osbuild-getting-started).
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
|
### OnPremise Development - Cockpit Build and Install
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The following scripts are used to build the frontend with Webpack and install it into the Cockpit directories. These scripts streamline the development process by automating build and installation steps.
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
#### 1. Build the Cockpit Frontend
|
||||||
|
|
||||||
|
Runs Webpack with the specified configuration (cockpit/webpack.config.ts) to build the frontend assets.
|
||||||
|
Use this command whenever you need to compile the latest changes in your frontend code.
|
||||||
|
|
||||||
|
Creates the necessary directory in the user's local Cockpit share (~/.local/share/cockpit/).
|
||||||
|
Creates a symbolic link (image-builder-frontend) pointing to the built frontend assets (cockpit/public).
|
||||||
|
Use this command after building the frontend to install it locally for development purposes.
|
||||||
|
The symbolic link allows Cockpit to serve the frontend assets from your local development environment,
|
||||||
|
making it easier to test changes in real-time without deploying to a remote server.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make devel-install
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
To uninstall and remove the symbolic link, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make devel-uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
For convenience, you can run the following to combine all three steps:
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make cockpit/all
|
||||||
|
```
|
||||||
|
|
||||||
### Quick Reference
|
### Quick Reference
|
||||||
| Directory | Description |
|
| Directory | Description |
|
||||||
| --------- | ----------- |
|
| --------- | ----------- |
|
||||||
| [`/api`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/api) | API schema and config files |
|
| [`/api`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/api) | API schema and config files |
|
||||||
| [`/config`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/config) | webpack configuration |
|
| [`/config`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/config) | webpack configuration |
|
||||||
|
| [`/devel`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/devel) | tools for local development |
|
||||||
| [`/src`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src) | source code |
|
| [`/src`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src) | source code |
|
||||||
| [`/src/Components`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src/Components) | source code split by individual components |
|
| [`/src/Components`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src/Components) | source code split by individual components |
|
||||||
| [`/src/test`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src/test) | test utilities |
|
| [`/src/test`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src/test) | test utilities |
|
||||||
|
|
@ -242,19 +272,8 @@ we're planning on using.
|
||||||
|
|
||||||
## Style Guidelines
|
## Style Guidelines
|
||||||
|
|
||||||
This project uses recommended rule sets rom several plugins:
|
This project uses eslint's recommended styling guidelines. These rules can be found here:
|
||||||
- `@eslint/js`
|
https://eslint.org/docs/rules/
|
||||||
- `typescript-eslint`
|
|
||||||
- `eslint-plugin-react`
|
|
||||||
- `eslint-plugin-react-hooks`
|
|
||||||
- `eslint-plugin-react-redux`
|
|
||||||
- `eslint-plugin-import`
|
|
||||||
- `eslint-plugin-jsx-a11y`
|
|
||||||
- `eslint-plugin-disable-autofix`
|
|
||||||
- `eslint-plugin-jest-dom`
|
|
||||||
- `eslint-plugin-testing-library`
|
|
||||||
- `eslint-plugin-playwright`
|
|
||||||
- `@redhat-cloud-services/eslint-config-redhat-cloud-services`
|
|
||||||
|
|
||||||
To run the linter, use:
|
To run the linter, use:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -263,10 +282,16 @@ npm run lint
|
||||||
|
|
||||||
Any errors that can be fixed automatically, can be corrected by running:
|
Any errors that can be fixed automatically, can be corrected by running:
|
||||||
```bash
|
```bash
|
||||||
npm run lint:js:fix
|
npm run lint --fix
|
||||||
```
|
```
|
||||||
|
|
||||||
All the linting rules and configuration of ESLint can be found in [`eslint.config.js`](https://github.com/RedHatInsights/image-builder-frontend/blob/main/eslint.config.js).
|
All the linting rules and configuration of eslint can be found in [`.eslintrc.yml`](https://github.com/RedHatInsights/image-builder-frontend/blob/main/.eslintrc.yml).
|
||||||
|
|
||||||
|
### Additional eslint rules
|
||||||
|
There are also additional rules added to enforce code style. Those being:
|
||||||
|
- `import/order` -> enforces the order in import statements and separates them into groups based on their type
|
||||||
|
- `prefer-const` -> enforces use of `const` declaration for variables that are never reassigned
|
||||||
|
- `no-console` -> throws an error for any calls of `console` methods leftover after debugging
|
||||||
|
|
||||||
## Test Guidelines
|
## Test Guidelines
|
||||||
|
|
||||||
|
|
@ -344,16 +369,16 @@ Follow these steps to find and paste the certification file into the 'Keychain A
|
||||||
npm ci
|
npm ci
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Download the Playwright browsers with
|
3. Download the Playwright browsers with
|
||||||
```bash
|
```bash
|
||||||
npx playwright install
|
npx playwright install
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Start the local development stage server by running
|
4. Start the local development stage server by running
|
||||||
```bash
|
```bash
|
||||||
npm run start:stage
|
npm run start:stage
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Now you have two options of how to run the tests:
|
5. Now you have two options of how to run the tests:
|
||||||
* (Preferred) Use VS Code and the [Playwright Test module for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). But other editors do have similar plugins for ease of use, if so desired
|
* (Preferred) Use VS Code and the [Playwright Test module for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). But other editors do have similar plugins for ease of use, if so desired
|
||||||
* Using terminal - `npx playwright test` will run the playwright test suite. `npx playwright test --headed` will run the suite in a vnc-like browser so you can watch it's interactions.
|
* Using terminal - `npx playwright test` will run the playwright test suite. `npx playwright test --headed` will run the suite in a vnc-like browser so you can watch it's interactions.
|
||||||
|
|
@ -5,6 +5,7 @@ npx @rtk-query/codegen-openapi ./api/config/imageBuilder.ts &
|
||||||
npx @rtk-query/codegen-openapi ./api/config/rhsm.ts &
|
npx @rtk-query/codegen-openapi ./api/config/rhsm.ts &
|
||||||
npx @rtk-query/codegen-openapi ./api/config/contentSources.ts &
|
npx @rtk-query/codegen-openapi ./api/config/contentSources.ts &
|
||||||
npx @rtk-query/codegen-openapi ./api/config/provisioning.ts &
|
npx @rtk-query/codegen-openapi ./api/config/provisioning.ts &
|
||||||
|
npx @rtk-query/codegen-openapi ./api/config/edge.ts &
|
||||||
npx @rtk-query/codegen-openapi ./api/config/compliance.ts &
|
npx @rtk-query/codegen-openapi ./api/config/compliance.ts &
|
||||||
npx @rtk-query/codegen-openapi ./api/config/composerCloudApi.ts &
|
npx @rtk-query/codegen-openapi ./api/config/composerCloudApi.ts &
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||||
|
|
||||||
const config: ConfigFile = {
|
const config: ConfigFile = {
|
||||||
schemaFile: 'https://console.redhat.com/api/compliance/v2/openapi.json',
|
schemaFile: '../schema/compliance.json',
|
||||||
apiFile: '../../src/store/service/emptyComplianceApi.ts',
|
apiFile: '../../src/store/service/emptyComplianceApi.ts',
|
||||||
apiImport: 'emptyComplianceApi',
|
apiImport: 'emptyComplianceApi',
|
||||||
outputFile: '../../src/store/service/complianceApi.ts',
|
outputFile: '../../src/store/service/complianceApi.ts',
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||||
|
|
||||||
const config: ConfigFile = {
|
const config: ConfigFile = {
|
||||||
schemaFile:
|
schemaFile: '../schema/composerCloudApi.v2.yaml',
|
||||||
'https://raw.githubusercontent.com/osbuild/osbuild-composer/main/internal/cloudapi/v2/openapi.v2.yml',
|
|
||||||
apiFile: '../../src/store/cockpit/emptyComposerCloudApi.ts',
|
apiFile: '../../src/store/cockpit/emptyComposerCloudApi.ts',
|
||||||
apiImport: 'emptyComposerCloudApi',
|
apiImport: 'emptyComposerCloudApi',
|
||||||
outputFile: '../../src/store/cockpit/composerCloudApi.ts',
|
outputFile: '../../src/store/cockpit/composerCloudApi.ts',
|
||||||
exportName: 'composerCloudApi',
|
exportName: 'composerCloudApi',
|
||||||
hooks: false,
|
hooks: false,
|
||||||
unionUndefined: true,
|
unionUndefined: true,
|
||||||
filterEndpoints: ['postCompose', 'getComposeStatus'],
|
filterEndpoints: [
|
||||||
|
'postCompose',
|
||||||
|
'getComposeStatus',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||||
|
|
||||||
const config: ConfigFile = {
|
const config: ConfigFile = {
|
||||||
schemaFile: 'https://console.redhat.com/api/content-sources/v1/openapi.json',
|
schemaFile: '../schema/contentSources.json',
|
||||||
apiFile: '../../src/store/service/emptyContentSourcesApi.ts',
|
apiFile: '../../src/store/service/emptyContentSourcesApi.ts',
|
||||||
apiImport: 'emptyContentSourcesApi',
|
apiImport: 'emptyContentSourcesApi',
|
||||||
outputFile: '../../src/store/service/contentSourcesApi.ts',
|
outputFile: '../../src/store/service/contentSourcesApi.ts',
|
||||||
|
|
@ -12,7 +12,6 @@ const config: ConfigFile = {
|
||||||
'createRepository',
|
'createRepository',
|
||||||
'listRepositories',
|
'listRepositories',
|
||||||
'listRepositoriesRpms',
|
'listRepositoriesRpms',
|
||||||
'listRepositoryParameters',
|
|
||||||
'searchRpm',
|
'searchRpm',
|
||||||
'searchPackageGroup',
|
'searchPackageGroup',
|
||||||
'listFeatures',
|
'listFeatures',
|
||||||
|
|
|
||||||
36
api/config/edge.ts
Normal file
36
api/config/edge.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||||
|
|
||||||
|
const config: ConfigFile = {
|
||||||
|
schemaFile: '../schema/edge.json',
|
||||||
|
apiFile: '../../src/store/service/emptyEdgeApi.ts',
|
||||||
|
apiImport: 'emptyEdgeApi',
|
||||||
|
outputFile: '../../src/store/service/edgeApi.ts',
|
||||||
|
exportName: 'edgeApi',
|
||||||
|
hooks: true,
|
||||||
|
unionUndefined: true,
|
||||||
|
filterEndpoints: [
|
||||||
|
'createImage',
|
||||||
|
'createImageUpdate',
|
||||||
|
'getAllImages',
|
||||||
|
'getImageStatusByID',
|
||||||
|
'getImageByID',
|
||||||
|
'getImageDetailsByID',
|
||||||
|
'getImageByOstree',
|
||||||
|
'createInstallerForImage',
|
||||||
|
'getRepoForImage',
|
||||||
|
'getMetadataForImage',
|
||||||
|
'createKickStartForImage',
|
||||||
|
'checkImageName',
|
||||||
|
'retryCreateImage',
|
||||||
|
'listAllImageSets',
|
||||||
|
'getImageSetsByID',
|
||||||
|
'getImageSetsView',
|
||||||
|
'getImageSetViewByID',
|
||||||
|
'getAllImageSetImagesView',
|
||||||
|
'getImageSetsDevicesByID',
|
||||||
|
'deleteImageSet',
|
||||||
|
'getImageSetImageView',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||||
|
|
||||||
const config: ConfigFile = {
|
const config: ConfigFile = {
|
||||||
schemaFile:
|
schemaFile: '../schema/imageBuilder.yaml',
|
||||||
'https://raw.githubusercontent.com/osbuild/image-builder/main/internal/v1/api.yaml',
|
|
||||||
apiFile: '../../src/store/service/emptyImageBuilderApi.ts',
|
apiFile: '../../src/store/service/emptyImageBuilderApi.ts',
|
||||||
apiImport: 'emptyImageBuilderApi',
|
apiImport: 'emptyImageBuilderApi',
|
||||||
outputFile: '../../src/store/service/imageBuilderApi.ts',
|
outputFile: '../../src/store/service/imageBuilderApi.ts',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||||
|
|
||||||
const config: ConfigFile = {
|
const config: ConfigFile = {
|
||||||
schemaFile: 'https://console.redhat.com/api/provisioning/v1/openapi.json',
|
schemaFile: '../schema/provisioning.json',
|
||||||
apiFile: '../../src/store/service/emptyProvisioningApi.ts',
|
apiFile: '../../src/store/service/emptyProvisioningApi.ts',
|
||||||
apiImport: 'emptyProvisioningApi',
|
apiImport: 'emptyProvisioningApi',
|
||||||
outputFile: '../../src/store/service/provisioningApi.ts',
|
outputFile: '../../src/store/service/provisioningApi.ts',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||||
|
|
||||||
const config: ConfigFile = {
|
const config: ConfigFile = {
|
||||||
schemaFile: 'https://console.redhat.com/api/rhsm/v2/openapi.json',
|
schemaFile: '../schema/rhsm.json',
|
||||||
apiFile: '../../src/store/service/emptyRhsmApi.ts',
|
apiFile: '../../src/store/service/emptyRhsmApi.ts',
|
||||||
apiImport: 'emptyRhsmApi',
|
apiImport: 'emptyRhsmApi',
|
||||||
outputFile: '../../src/store/service/rhsmApi.ts',
|
outputFile: '../../src/store/service/rhsmApi.ts',
|
||||||
|
|
|
||||||
10
api/pull.sh
Normal file
10
api/pull.sh
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Download the most up-to-date imageBuilder.yaml file and overwrite the existing one
|
||||||
|
curl https://raw.githubusercontent.com/osbuild/image-builder/main/internal/v1/api.yaml -o ./api/schema/imageBuilder.yaml
|
||||||
|
|
||||||
|
curl https://console.redhat.com/api/compliance/v2/openapi.json -o ./api/schema/compliance.json
|
||||||
|
|
||||||
|
curl https://console.redhat.com/api/content-sources/v1/openapi.json -o ./api/schema/contentSources.json
|
||||||
|
|
||||||
|
curl https://raw.githubusercontent.com/osbuild/osbuild-composer/main/internal/cloudapi/v2/openapi.v2.yml -o ./api/schema/composerCloudApi.v2.yaml
|
||||||
17629
api/schema/compliance.json
Normal file
17629
api/schema/compliance.json
Normal file
File diff suppressed because it is too large
Load diff
2764
api/schema/composerCloudApi.v2.yaml
Normal file
2764
api/schema/composerCloudApi.v2.yaml
Normal file
File diff suppressed because it is too large
Load diff
6657
api/schema/contentSources.json
Normal file
6657
api/schema/contentSources.json
Normal file
File diff suppressed because it is too large
Load diff
5722
api/schema/edge.json
Normal file
5722
api/schema/edge.json
Normal file
File diff suppressed because it is too large
Load diff
2270
api/schema/imageBuilder.yaml
Normal file
2270
api/schema/imageBuilder.yaml
Normal file
File diff suppressed because it is too large
Load diff
2044
api/schema/provisioning.json
Normal file
2044
api/schema/provisioning.json
Normal file
File diff suppressed because it is too large
Load diff
1
api/schema/rhsm.json
Normal file
1
api/schema/rhsm.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
Subproject commit b496d0a8c1755608bd256a6960869b14a7689d38
|
Subproject commit 75adad05c9e22ff84c7d3b43564554a26f55a8a9
|
||||||
|
|
@ -9,7 +9,7 @@ export COMPONENT="image-builder"
|
||||||
export IMAGE="quay.io/cloudservices/image-builder-frontend"
|
export IMAGE="quay.io/cloudservices/image-builder-frontend"
|
||||||
export APP_ROOT=$(pwd)
|
export APP_ROOT=$(pwd)
|
||||||
export WORKSPACE=${WORKSPACE:-$APP_ROOT} # if running in jenkins, use the build's workspace
|
export WORKSPACE=${WORKSPACE:-$APP_ROOT} # if running in jenkins, use the build's workspace
|
||||||
export NODE_BUILD_VERSION=22
|
export NODE_BUILD_VERSION=18
|
||||||
COMMON_BUILDER=https://raw.githubusercontent.com/RedHatInsights/insights-frontend-builder-common/master
|
COMMON_BUILDER=https://raw.githubusercontent.com/RedHatInsights/insights-frontend-builder-common/master
|
||||||
|
|
||||||
set -exv
|
set -exv
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
# cockpit-image-builder
|
TODO
|
||||||
|
|
||||||
The "cockpit-image-builder" provides an on-premise frontend for image building, designed to integrate with [Cockpit](https://cockpit-project.org/) as a plugin. It allows users to create, manage, and compose custom operating system images, with images stored locally.
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Name: cockpit-image-builder
|
Name: cockpit-image-builder
|
||||||
Version: 76
|
Version: 68
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Image builder plugin for Cockpit
|
Summary: Image builder plugin for Cockpit
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en-us" class="layout-pf pf-m-redhat-font">
|
<html lang="en-us" class="layout-pf pf-m-redhat-font">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8">
|
||||||
<title>Image-Builder</title>
|
<title>Image-Builder</title>
|
||||||
|
|
||||||
<!-- js dependencies -->
|
<!-- js dependencies -->
|
||||||
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
||||||
<script defer src="main.js"></script>
|
<link href="main.css" rel="stylesheet">
|
||||||
<link href="main.css" rel="stylesheet" />
|
</head>
|
||||||
</head>
|
<body>
|
||||||
<body>
|
<div id="main"></div>
|
||||||
<div class="ct-page-fill" id="main"></div>
|
<script defer src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -75,15 +75,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.scss$/,
|
test: /\.scss$/,
|
||||||
use: [
|
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
|
||||||
MiniCssExtractPlugin.loader,
|
|
||||||
|
|
||||||
{
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: { url: false },
|
|
||||||
},
|
|
||||||
'sass-loader',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
183
deploy/iqe-trigger-integration.yml
Normal file
183
deploy/iqe-trigger-integration.yml
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
---
|
||||||
|
apiVersion: template.openshift.io/v1
|
||||||
|
kind: Template
|
||||||
|
metadata:
|
||||||
|
name: image-builder-frontend-tests
|
||||||
|
objects:
|
||||||
|
- apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: image-builder-frontend-${TEST_TYPE}-tests-${IMAGE_TAG}-${UID}
|
||||||
|
annotations:
|
||||||
|
"ignore-check.kube-linter.io/no-liveness-probe": "probes not required on Job pods"
|
||||||
|
"ignore-check.kube-linter.io/no-readiness-probe": "probes not required on Job pods"
|
||||||
|
spec:
|
||||||
|
backoffLimit: 0
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: quay-cloudservices-pull
|
||||||
|
restartPolicy: Never
|
||||||
|
volumes:
|
||||||
|
- name: sel-shm
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
- name: sel-downloads
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
sizeLimit: 64Mi
|
||||||
|
containers:
|
||||||
|
- name: image-builder-frontend-iqe-${TEST_TYPE}-tests-${IMAGE_TAG}-${UID}
|
||||||
|
image: ${IQE_IMAGE}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
args:
|
||||||
|
- run
|
||||||
|
env:
|
||||||
|
- name: ENV_FOR_DYNACONF
|
||||||
|
value: ${ENV_FOR_DYNACONF}
|
||||||
|
- name: DYNACONF_MAIN__use_beta
|
||||||
|
value: ${USE_BETA}
|
||||||
|
- name: IQE_IBUTSU_SOURCE
|
||||||
|
value: image-builder-${IMAGE_TAG}-tests-${UID}-${ENV_FOR_DYNACONF}
|
||||||
|
- name: IQE_BROWSERLOG
|
||||||
|
value: ${IQE_BROWSERLOG}
|
||||||
|
- name: IQE_NETLOG
|
||||||
|
value: ${IQE_NETLOG}
|
||||||
|
- name: IQE_PLUGINS
|
||||||
|
value: ${IQE_PLUGINS}
|
||||||
|
- name: IQE_MARKER_EXPRESSION
|
||||||
|
value: ${IQE_MARKER_EXPRESSION}
|
||||||
|
- name: IQE_FILTER_EXPRESSION
|
||||||
|
value: ${IQE_FILTER_EXPRESSION}
|
||||||
|
- name: IQE_LOG_LEVEL
|
||||||
|
value: ${IQE_LOG_LEVEL}
|
||||||
|
- name: IQE_REQUIREMENTS
|
||||||
|
value: ${IQE_REQUIREMENTS}
|
||||||
|
- name: IQE_PARALLEL_ENABLED
|
||||||
|
value: ${IQE_PARALLEL_ENABLED}
|
||||||
|
- name: IQE_REQUIREMENTS_PRIORITY
|
||||||
|
value: ${IQE_REQUIREMENTS_PRIORITY}
|
||||||
|
- name: IQE_TEST_IMPORTANCE
|
||||||
|
value: ${IQE_TEST_IMPORTANCE}
|
||||||
|
- name: DYNACONF_IQE_VAULT_LOADER_ENABLED
|
||||||
|
value: "true"
|
||||||
|
- name: DYNACONF_IQE_VAULT_VERIFY
|
||||||
|
value: "true"
|
||||||
|
- name: DYNACONF_IQE_VAULT_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
key: url
|
||||||
|
name: iqe-vault
|
||||||
|
optional: true
|
||||||
|
- name: DYNACONF_IQE_VAULT_MOUNT_POINT
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
key: mountPoint
|
||||||
|
name: iqe-vault
|
||||||
|
optional: true
|
||||||
|
- name: DYNACONF_IQE_VAULT_ROLE_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
key: roleId
|
||||||
|
name: iqe-vault
|
||||||
|
optional: true
|
||||||
|
- name: DYNACONF_IQE_VAULT_SECRET_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
key: secretId
|
||||||
|
name: iqe-vault
|
||||||
|
optional: true
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: ${IQE_CPU_LIMIT}
|
||||||
|
memory: ${IQE_MEMORY_LIMIT}
|
||||||
|
requests:
|
||||||
|
cpu: ${IQE_CPU_REQUEST}
|
||||||
|
memory: ${IQE_MEMORY_REQUEST}
|
||||||
|
terminationMessagePath: /dev/termination-log
|
||||||
|
terminationMessagePolicy: File
|
||||||
|
volumeMounts:
|
||||||
|
- name: sel-downloads
|
||||||
|
mountPath: /sel-downloads
|
||||||
|
- name: image-builder-sel-${TEST_TYPE}-tests-${IMAGE_TAG}-${UID}
|
||||||
|
image: ${IQE_SEL_IMAGE}
|
||||||
|
env:
|
||||||
|
- name: _JAVA_OPTIONS
|
||||||
|
value: ${SELENIUM_JAVA_OPTS}
|
||||||
|
- name: VNC_GEOMETRY
|
||||||
|
value: ${VNC_GEOMETRY}
|
||||||
|
- name: SE_NODE_SESSION_TIMEOUT
|
||||||
|
value: ${SE_NODE_SESSION_TIMEOUT}
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: ${SELENIUM_CPU_LIMIT}
|
||||||
|
memory: ${SELENIUM_MEMORY_LIMIT}
|
||||||
|
requests:
|
||||||
|
cpu: ${SELENIUM_CPU_REQUEST}
|
||||||
|
memory: ${SELENIUM_MEMORY_REQUEST}
|
||||||
|
volumeMounts:
|
||||||
|
- name: sel-shm
|
||||||
|
mountPath: /dev/shm
|
||||||
|
- name: sel-downloads
|
||||||
|
mountPath: /home/selenium/Downloads
|
||||||
|
parameters:
|
||||||
|
- name: IMAGE_TAG
|
||||||
|
value: ''
|
||||||
|
required: true
|
||||||
|
- name: UID
|
||||||
|
description: "Unique job name suffix"
|
||||||
|
generate: expression
|
||||||
|
from: "[a-z0-9]{6}"
|
||||||
|
- name: IQE_IMAGE
|
||||||
|
description: "container image path for the iqe plugin"
|
||||||
|
value: quay.io/cloudservices/iqe-tests:insights-experiences
|
||||||
|
- name: ENV_FOR_DYNACONF
|
||||||
|
value: stage_proxy
|
||||||
|
- name: USE_BETA
|
||||||
|
value: "true"
|
||||||
|
- name: IQE_PLUGINS
|
||||||
|
value: insights_experiences
|
||||||
|
- name: IQE_MARKER_EXPRESSION
|
||||||
|
value: 'image_builder'
|
||||||
|
- name: IQE_FILTER_EXPRESSION
|
||||||
|
value: ''
|
||||||
|
- name: IQE_LOG_LEVEL
|
||||||
|
value: info
|
||||||
|
- name: IQE_REQUIREMENTS
|
||||||
|
value: ''
|
||||||
|
- name: IQE_REQUIREMENTS_PRIORITY
|
||||||
|
value: ''
|
||||||
|
- name: IQE_TEST_IMPORTANCE
|
||||||
|
value: ''
|
||||||
|
- name: IQE_SEL_IMAGE
|
||||||
|
value: 'quay.io/redhatqe/selenium-standalone:ff_91.9.1esr_chrome_103.0.5060.114'
|
||||||
|
- name: IQE_BROWSERLOG
|
||||||
|
value: "1"
|
||||||
|
- name: IQE_NETLOG
|
||||||
|
value: "1"
|
||||||
|
- name: TEST_TYPE
|
||||||
|
value: ''
|
||||||
|
- name: IQE_CPU_LIMIT
|
||||||
|
value: "1"
|
||||||
|
- name: IQE_MEMORY_LIMIT
|
||||||
|
value: 1.5Gi
|
||||||
|
- name: IQE_CPU_REQUEST
|
||||||
|
value: 250m
|
||||||
|
- name: IQE_MEMORY_REQUEST
|
||||||
|
value: 1Gi
|
||||||
|
- name: SELENIUM_CPU_LIMIT
|
||||||
|
value: 500m
|
||||||
|
- name: SELENIUM_MEMORY_LIMIT
|
||||||
|
value: 2Gi
|
||||||
|
- name: SELENIUM_CPU_REQUEST
|
||||||
|
value: 100m
|
||||||
|
- name: SELENIUM_MEMORY_REQUEST
|
||||||
|
value: 1Gi
|
||||||
|
- name: SELENIUM_JAVA_OPTS
|
||||||
|
value: ''
|
||||||
|
- name: VNC_GEOMETRY
|
||||||
|
value: '1920x1080'
|
||||||
|
- name: IQE_PARALLEL_ENABLED
|
||||||
|
value: "false"
|
||||||
|
- name: SE_NODE_SESSION_TIMEOUT
|
||||||
|
value: "600"
|
||||||
12
distribution/Dockerfile
Normal file
12
distribution/Dockerfile
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
FROM node:18
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
EXPOSE 8002
|
||||||
|
EXPOSE 1337
|
||||||
|
|
||||||
|
CMD [ "npm", "run", "devel" ]
|
||||||
174
eslint.config.js
174
eslint.config.js
|
|
@ -1,174 +0,0 @@
|
||||||
const js = require('@eslint/js');
|
|
||||||
const tseslint = require('typescript-eslint');
|
|
||||||
const pluginReact = require('eslint-plugin-react');
|
|
||||||
const pluginReactHooks = require('eslint-plugin-react-hooks');
|
|
||||||
const pluginReactRedux = require('eslint-plugin-react-redux');
|
|
||||||
const pluginImport = require('eslint-plugin-import');
|
|
||||||
const fecConfig = require('@redhat-cloud-services/eslint-config-redhat-cloud-services');
|
|
||||||
const pluginJsxA11y = require('eslint-plugin-jsx-a11y');
|
|
||||||
const disableAutofix = require('eslint-plugin-disable-autofix');
|
|
||||||
const pluginPrettier = require('eslint-plugin-prettier');
|
|
||||||
const jestDom = require('eslint-plugin-jest-dom');
|
|
||||||
const pluginTestingLibrary = require('eslint-plugin-testing-library');
|
|
||||||
const pluginPlaywright = require('eslint-plugin-playwright');
|
|
||||||
const { defineConfig } = require('eslint/config');
|
|
||||||
const globals = require('globals');
|
|
||||||
|
|
||||||
module.exports = defineConfig([
|
|
||||||
{ // Ignore programatically generated files
|
|
||||||
ignores: [
|
|
||||||
'**/mockServiceWorker.js',
|
|
||||||
'**/imageBuilderApi.ts',
|
|
||||||
'**/contentSourcesApi.ts',
|
|
||||||
'**/rhsmApi.ts',
|
|
||||||
'**/provisioningApi.ts',
|
|
||||||
'**/complianceApi.ts',
|
|
||||||
'**/composerCloudApi.ts'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{ // Base config for js/ts files
|
|
||||||
files: ['**/*.{js,ts,jsx,tsx}'],
|
|
||||||
languageOptions: {
|
|
||||||
parser: tseslint.parser,
|
|
||||||
parserOptions: {
|
|
||||||
project: './tsconfig.json'
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
...globals.browser,
|
|
||||||
// node
|
|
||||||
'JSX': 'readonly',
|
|
||||||
'process': 'readonly',
|
|
||||||
'__dirname': 'readonly',
|
|
||||||
'require': 'readonly',
|
|
||||||
// vitest
|
|
||||||
'describe': 'readonly',
|
|
||||||
'it': 'readonly',
|
|
||||||
'test': 'readonly',
|
|
||||||
'expect': 'readonly',
|
|
||||||
'vi': 'readonly',
|
|
||||||
'beforeAll': 'readonly',
|
|
||||||
'beforeEach': 'readonly',
|
|
||||||
'afterAll': 'readonly',
|
|
||||||
'afterEach': 'readonly'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
js,
|
|
||||||
'@typescript-eslint': tseslint.plugin,
|
|
||||||
react: pluginReact,
|
|
||||||
'react-hooks': pluginReactHooks,
|
|
||||||
'react-redux': pluginReactRedux,
|
|
||||||
import: pluginImport,
|
|
||||||
jsxA11y: pluginJsxA11y,
|
|
||||||
'disable-autofix': disableAutofix,
|
|
||||||
prettier: pluginPrettier,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...js.configs.recommended.rules,
|
|
||||||
...tseslint.configs.recommended.rules,
|
|
||||||
...pluginReact.configs.flat.recommended.rules,
|
|
||||||
...pluginReactHooks.configs.recommended.rules,
|
|
||||||
...pluginReactRedux.configs.recommended.rules,
|
|
||||||
...fecConfig.rules,
|
|
||||||
'import/order': ['error', {
|
|
||||||
groups: ['builtin', 'external', 'internal', 'sibling', 'parent', 'index'],
|
|
||||||
alphabetize: {
|
|
||||||
order: 'asc',
|
|
||||||
caseInsensitive: true
|
|
||||||
},
|
|
||||||
'newlines-between': 'always',
|
|
||||||
pathGroups: [ // ensures the import of React is always on top
|
|
||||||
{
|
|
||||||
pattern: 'react',
|
|
||||||
group: 'builtin',
|
|
||||||
position: 'before'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pathGroupsExcludedImportTypes: ['react']
|
|
||||||
}],
|
|
||||||
'sort-imports': ['error', {
|
|
||||||
ignoreCase: true,
|
|
||||||
ignoreDeclarationSort: true,
|
|
||||||
ignoreMemberSort: false,
|
|
||||||
}],
|
|
||||||
'no-duplicate-imports': 'error',
|
|
||||||
'prefer-const': ['error', {
|
|
||||||
destructuring: 'any',
|
|
||||||
}],
|
|
||||||
'no-console': 'error',
|
|
||||||
'eqeqeq': 'error',
|
|
||||||
'array-callback-return': 'warn',
|
|
||||||
'@typescript-eslint/ban-ts-comment': ['error', {
|
|
||||||
'ts-expect-error': 'allow-with-description',
|
|
||||||
'ts-ignore': 'allow-with-description',
|
|
||||||
'ts-nocheck': true,
|
|
||||||
'ts-check': true,
|
|
||||||
minimumDescriptionLength: 5,
|
|
||||||
}],
|
|
||||||
'@typescript-eslint/ban-types': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'error',
|
|
||||||
'@typescript-eslint/no-unsafe-function-type': 'error',
|
|
||||||
'@typescript-eslint/no-require-imports': 'error',
|
|
||||||
'disable-autofix/@typescript-eslint/no-unnecessary-condition': 'warn',
|
|
||||||
'no-unused-vars': 'off', // disable js rule in favor of @typescript-eslint's rule
|
|
||||||
'@typescript-eslint/no-unused-vars': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
argsIgnorePattern: '^_',
|
|
||||||
varsIgnorePattern: '^_',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'jsx-a11y/no-autofocus': 'off',
|
|
||||||
'prettier/prettier': ['error', {
|
|
||||||
semi: true,
|
|
||||||
tabWidth: 2,
|
|
||||||
singleQuote: true,
|
|
||||||
jsxSingleQuote: true,
|
|
||||||
bracketSpacing: true,
|
|
||||||
tsxSingleQuote: true,
|
|
||||||
tsSingleQuote: true,
|
|
||||||
printWidth: 80,
|
|
||||||
trailingComma: 'all',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect', // Automatically detect React version
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{ // Override for test files
|
|
||||||
files: ['src/test/**/*.{ts,tsx}'],
|
|
||||||
plugins: {
|
|
||||||
'jest-dom': jestDom,
|
|
||||||
'testing-library': pluginTestingLibrary,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...jestDom.configs.recommended.rules,
|
|
||||||
...pluginTestingLibrary.configs.react.rules,
|
|
||||||
'react/display-name': 'off',
|
|
||||||
'react/prop-types': 'off',
|
|
||||||
'testing-library/no-debugging-utils': 'error'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{ // Override for Playwright tests
|
|
||||||
files: ['playwright/**/*.ts'],
|
|
||||||
plugins: {
|
|
||||||
playwright: pluginPlaywright,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...pluginPlaywright.configs.recommended.rules,
|
|
||||||
'playwright/no-conditional-in-test': 'off',
|
|
||||||
'playwright/no-conditional-expect': 'off',
|
|
||||||
'playwright/no-skipped-test': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
'allowConditional': true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
4642
package-lock.json
generated
4642
package-lock.json
generated
File diff suppressed because it is too large
Load diff
75
package.json
75
package.json
|
|
@ -8,18 +8,17 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ltd/j-toml": "1.38.0",
|
"@ltd/j-toml": "1.38.0",
|
||||||
"@patternfly/patternfly": "6.3.1",
|
"@patternfly/patternfly": "5.4.1",
|
||||||
"@patternfly/react-code-editor": "6.3.1",
|
"@patternfly/react-code-editor": "5.4.1",
|
||||||
"@patternfly/react-core": "6.3.1",
|
"@patternfly/react-core": "5.4.12",
|
||||||
"@patternfly/react-table": "6.3.1",
|
"@patternfly/react-table": "5.4.14",
|
||||||
"@redhat-cloud-services/frontend-components": "7.0.3",
|
"@redhat-cloud-services/frontend-components": "5.2.6",
|
||||||
"@redhat-cloud-services/frontend-components-notifications": "6.1.5",
|
"@redhat-cloud-services/frontend-components-notifications": "4.1.20",
|
||||||
"@redhat-cloud-services/frontend-components-utilities": "7.0.3",
|
"@redhat-cloud-services/frontend-components-utilities": "5.0.11",
|
||||||
"@redhat-cloud-services/types": "3.0.1",
|
|
||||||
"@reduxjs/toolkit": "2.8.2",
|
"@reduxjs/toolkit": "2.8.2",
|
||||||
"@scalprum/react-core": "0.9.5",
|
"@scalprum/react-core": "0.9.5",
|
||||||
"@sentry/webpack-plugin": "4.1.1",
|
"@sentry/webpack-plugin": "3.4.0",
|
||||||
"@unleash/proxy-client-react": "5.0.1",
|
"@unleash/proxy-client-react": "5.0.0",
|
||||||
"classnames": "2.5.1",
|
"classnames": "2.5.1",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
|
@ -31,72 +30,68 @@
|
||||||
"redux-promise-middleware": "6.2.0"
|
"redux-promise-middleware": "6.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.0",
|
"@babel/core": "7.26.10",
|
||||||
"@babel/preset-env": "7.28.0",
|
"@babel/preset-env": "7.27.2",
|
||||||
"@babel/preset-react": "7.27.1",
|
"@babel/preset-react": "7.27.1",
|
||||||
"@babel/preset-typescript": "7.27.1",
|
"@babel/preset-typescript": "7.27.0",
|
||||||
"@currents/playwright": "1.15.3",
|
"@currents/playwright": "1.13.2",
|
||||||
"@eslint/js": "9.32.0",
|
"@patternfly/react-icons": "5.4.2",
|
||||||
"@patternfly/react-icons": "6.3.1",
|
|
||||||
"@playwright/test": "1.51.1",
|
"@playwright/test": "1.51.1",
|
||||||
"@redhat-cloud-services/eslint-config-redhat-cloud-services": "3.0.0",
|
"@redhat-cloud-services/eslint-config-redhat-cloud-services": "2.0.12",
|
||||||
"@redhat-cloud-services/frontend-components-config": "6.3.8",
|
"@redhat-cloud-services/frontend-components-config": "6.3.8",
|
||||||
"@redhat-cloud-services/tsc-transform-imports": "1.0.25",
|
"@redhat-cloud-services/tsc-transform-imports": "1.0.24",
|
||||||
"@rtk-query/codegen-openapi": "2.0.0",
|
"@rtk-query/codegen-openapi": "2.0.0",
|
||||||
"@testing-library/dom": "10.4.1",
|
"@testing-library/dom": "10.4.0",
|
||||||
"@testing-library/jest-dom": "6.6.4",
|
"@testing-library/jest-dom": "6.6.3",
|
||||||
"@testing-library/react": "16.3.0",
|
"@testing-library/react": "16.3.0",
|
||||||
"@testing-library/user-event": "14.6.1",
|
"@testing-library/user-event": "14.6.1",
|
||||||
"@types/node": "24.3.0",
|
"@types/node": "22.15.1",
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "18.3.12",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "18.3.1",
|
||||||
"@types/react-redux": "7.1.34",
|
"@types/react-redux": "7.1.34",
|
||||||
"@types/uuid": "10.0.0",
|
"@types/uuid": "10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||||
"@typescript-eslint/parser": "8.40.0",
|
"@typescript-eslint/parser": "8.32.1",
|
||||||
"@vitejs/plugin-react": "4.7.0",
|
"@vitejs/plugin-react": "4.4.1",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.1.2",
|
||||||
"babel-loader": "10.0.0",
|
"babel-loader": "10.0.0",
|
||||||
"chart.js": "4.5.0",
|
"chart.js": "4.4.9",
|
||||||
"chartjs-adapter-moment": "1.0.1",
|
"chartjs-adapter-moment": "1.0.1",
|
||||||
"chartjs-plugin-annotation": "3.1.0",
|
"chartjs-plugin-annotation": "3.1.0",
|
||||||
"copy-webpack-plugin": "13.0.0",
|
"copy-webpack-plugin": "13.0.0",
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"eslint": "9.33.0",
|
"eslint": "8.57.1",
|
||||||
"eslint-plugin-disable-autofix": "5.0.1",
|
"eslint-plugin-disable-autofix": "5.0.1",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.31.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.0",
|
||||||
"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",
|
||||||
"eslint-plugin-testing-library": "7.6.6",
|
"eslint-plugin-testing-library": "7.2.2",
|
||||||
"git-revision-webpack-plugin": "5.0.0",
|
"git-revision-webpack-plugin": "5.0.0",
|
||||||
"globals": "16.3.0",
|
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"identity-obj-proxy": "3.0.0",
|
"identity-obj-proxy": "3.0.0",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"madge": "8.0.0",
|
"madge": "8.0.0",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"msw": "2.10.5",
|
"msw": "2.7.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",
|
||||||
"react-chartjs-2": "5.3.0",
|
"react-chartjs-2": "5.3.0",
|
||||||
"redux-mock-store": "1.5.5",
|
"redux-mock-store": "1.5.5",
|
||||||
"sass": "1.90.0",
|
"sass": "1.88.0",
|
||||||
"sass-loader": "16.0.5",
|
"sass-loader": "16.0.5",
|
||||||
"stylelint": "16.23.1",
|
"stylelint": "16.18.0",
|
||||||
"stylelint-config-recommended-scss": "16.0.0",
|
"stylelint-config-recommended-scss": "14.1.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.40.0",
|
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"vitest": "3.2.4",
|
"vitest": "3.1.2",
|
||||||
"vitest-canvas-mock": "0.3.3",
|
"vitest-canvas-mock": "0.3.3",
|
||||||
"webpack-bundle-analyzer": "4.10.2",
|
"webpack-bundle-analyzer": "4.10.2",
|
||||||
"whatwg-fetch": "3.6.20"
|
"whatwg-fetch": "3.6.20"
|
||||||
|
|
@ -117,7 +112,9 @@
|
||||||
"test:cockpit": "src/test/cockpit-tests.sh",
|
"test:cockpit": "src/test/cockpit-tests.sh",
|
||||||
"build": "fec build",
|
"build": "fec build",
|
||||||
"build:cockpit": "webpack --config cockpit/webpack.config.ts",
|
"build:cockpit": "webpack --config cockpit/webpack.config.ts",
|
||||||
"api": "bash api/codegen.sh",
|
"api": "npm-run-all api:pull api:generate",
|
||||||
|
"api:generate": "bash api/codegen.sh",
|
||||||
|
"api:pull": "bash api/pull.sh",
|
||||||
"verify": "npm-run-all build lint test",
|
"verify": "npm-run-all build lint test",
|
||||||
"postinstall": "ts-patch install",
|
"postinstall": "ts-patch install",
|
||||||
"circular": "madge --circular ./src --extensions js,ts,tsx",
|
"circular": "madge --circular ./src --extensions js,ts,tsx",
|
||||||
|
|
|
||||||
10
packit.yaml
10
packit.yaml
|
|
@ -16,15 +16,6 @@ srpm_build_deps:
|
||||||
- npm
|
- npm
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: tests
|
|
||||||
identifier: self
|
|
||||||
trigger: pull_request
|
|
||||||
tmt_plan: /plans/all/main
|
|
||||||
targets:
|
|
||||||
- centos-stream-10
|
|
||||||
- fedora-41
|
|
||||||
- fedora-42
|
|
||||||
|
|
||||||
- job: copr_build
|
- job: copr_build
|
||||||
trigger: pull_request
|
trigger: pull_request
|
||||||
targets: &build_targets
|
targets: &build_targets
|
||||||
|
|
@ -33,6 +24,7 @@ jobs:
|
||||||
- centos-stream-10
|
- centos-stream-10
|
||||||
- centos-stream-10-aarch64
|
- centos-stream-10-aarch64
|
||||||
- fedora-all
|
- fedora-all
|
||||||
|
- fedora-all-aarch64
|
||||||
|
|
||||||
- job: copr_build
|
- job: copr_build
|
||||||
trigger: commit
|
trigger: commit
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
summary: cockpit-image-builder playwright tests
|
|
||||||
prepare:
|
|
||||||
how: install
|
|
||||||
package:
|
|
||||||
- cockpit-image-builder
|
|
||||||
discover:
|
|
||||||
how: fmf
|
|
||||||
execute:
|
|
||||||
how: tmt
|
|
||||||
|
|
||||||
/main:
|
|
||||||
summary: playwright tests
|
|
||||||
discover+:
|
|
||||||
test: /schutzbot/playwright
|
|
||||||
|
|
@ -34,14 +34,9 @@ export default defineConfig({
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'chromium',
|
||||||
use: {
|
use: { ...devices['Desktop Chrome'] },
|
||||||
...devices['Desktop Chrome'],
|
|
||||||
storageState: '.auth/user.json',
|
|
||||||
},
|
|
||||||
dependencies: ['setup'],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { test } from '../fixtures/customizations';
|
|
||||||
import { isHosted } from '../helpers/helpers';
|
|
||||||
import { ensureAuthenticated } from '../helpers/login';
|
|
||||||
import {
|
|
||||||
ibFrame,
|
|
||||||
navigateToLandingPage,
|
|
||||||
navigateToOptionalSteps,
|
|
||||||
} from '../helpers/navHelpers';
|
|
||||||
import {
|
|
||||||
createBlueprint,
|
|
||||||
deleteBlueprint,
|
|
||||||
exportBlueprint,
|
|
||||||
fillInDetails,
|
|
||||||
fillInImageOutputGuest,
|
|
||||||
importBlueprint,
|
|
||||||
registerLater,
|
|
||||||
} from '../helpers/wizardHelpers';
|
|
||||||
|
|
||||||
const validCallbackUrl =
|
|
||||||
'https://controller.url/api/controller/v2/job_templates/9/callback/';
|
|
||||||
const validHttpCallbackUrl =
|
|
||||||
'http://controller.url/api/controller/v2/job_templates/9/callback/';
|
|
||||||
const validHostConfigKey = 'hostconfigkey';
|
|
||||||
const validCertificate = `-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDXTCCAkWgAwIBAgIJAOEzx5ezZ9EIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
|
||||||
BAYTAklOMQswCQYDVQQIDAJLUjEMMAoGA1UEBwwDS1JHMRAwDgYDVQQKDAdUZXN0
|
|
||||||
IENBMB4XDTI1MDUxNTEyMDAwMFoXDTI2MDUxNTEyMDAwMFowRTELMAkGA1UEBhMC
|
|
||||||
SU4xCzAJBgNVBAgMAktSMQwwCgYDVQQHDANSR0sxEDAOBgNVBAoMB1Rlc3QgQ0Ew
|
|
||||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+R4gfN5pyJQo5qBTTtN+7
|
|
||||||
eE9CSXZJ8SVVaE3U54IgqQoqsSoBY5QtExy7v5C6l6mW4E6dzK/JecmvTTO/BvlG
|
|
||||||
A5k2hxB6bOQxtxYwfgElH+RFWN9P4xxhtEiQgHoG1rDfnXuDJk1U3YEkCQELUebz
|
|
||||||
fF3EIDU1yR0Sz2bA+Sl2VXe8og1MEZfytq8VZUVltxtn2PfW7zI5gOllBR2sKeUc
|
|
||||||
K6h8HXN7qMgfEvsLIXxTw7fU/zA3ibcxfRCl3m6QhF8hwRh6F9Wtz2s8hCzGegV5
|
|
||||||
z0M39nY7X8C3GZQ4Ly8v8DdY+FbEix7K3SSBRbWtdPfAHRFlX9Er2Wf8DAr7O2hH
|
|
||||||
AgMBAAGjUDBOMB0GA1UdDgQWBBTXXz2eIDgK+BhzDUAGzptn0OMcpDAfBgNVHSME
|
|
||||||
GDAWgBTXXz2eIDgK+BhzDUAGzptn0OMcpDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
|
|
||||||
DQEBCwUAA4IBAQAoUgY4jsuBMB3el9cc7JS2rcOhhJzn47Hj2UANfJq52g5lbjo7
|
|
||||||
XDc7Wb3VDcV+1LzjdzayT1qO1WzHb6FDPW9L9f6h4s8lj6MvJ+xhOWgD11srdIt3
|
|
||||||
vbQaQW4zDfeVRcKXzqbcUX8BLXAdzJPqVwZ+Z4EDjYrJ7lF9k+IqfZm0MsYX7el9
|
|
||||||
kvdRHbLuF4Q0sZ05CXMFkhM0Ulhu4MZ+1FcsQa7nWfZzTmbjHOuWJPB4z5WwrB7z
|
|
||||||
U8YYvWJ3qxToWGbATqJxkRKGGqLrNrmwcfzgPqkpuCRYi0Kky6gJ1RvL+DRopY9x
|
|
||||||
uD+ckf3oH2wYAB6RpPRMkfVxe7lGMvq/yEZ6
|
|
||||||
-----END CERTIFICATE-----`;
|
|
||||||
const invalidCertificate = `-----BEGIN CERTIFICATE-----
|
|
||||||
ThisIs*Not+Valid/Base64==
|
|
||||||
-----END CERTIFICATE-----`;
|
|
||||||
|
|
||||||
test('Create a blueprint with AAP registration customization', async ({
|
|
||||||
page,
|
|
||||||
cleanup,
|
|
||||||
}) => {
|
|
||||||
const blueprintName = 'test-' + uuidv4();
|
|
||||||
|
|
||||||
// Skip entirely in Cockpit/on-premise where AAP customization is unavailable
|
|
||||||
test.skip(!isHosted(), 'AAP customization is not available in the plugin');
|
|
||||||
|
|
||||||
// Delete the blueprint after the run fixture
|
|
||||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
|
||||||
await ensureAuthenticated(page);
|
|
||||||
|
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
|
||||||
|
|
||||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
|
||||||
await navigateToOptionalSteps(frame);
|
|
||||||
await registerLater(frame);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Select and fill the AAP step with valid configuration', async () => {
|
|
||||||
await frame
|
|
||||||
.getByRole('button', { name: 'Ansible Automation Platform' })
|
|
||||||
.click();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'ansible callback url' })
|
|
||||||
.fill(validCallbackUrl);
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'host config key' })
|
|
||||||
.fill(validHostConfigKey);
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'File upload' })
|
|
||||||
.fill(validCertificate);
|
|
||||||
await expect(frame.getByRole('button', { name: 'Next' })).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Test TLS confirmation checkbox for HTTPS URLs', async () => {
|
|
||||||
// TLS confirmation checkbox should appear for HTTPS URLs
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('checkbox', {
|
|
||||||
name: 'Insecure',
|
|
||||||
}),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
// Check TLS confirmation and verify CA input is hidden
|
|
||||||
await frame
|
|
||||||
.getByRole('checkbox', {
|
|
||||||
name: 'Insecure',
|
|
||||||
})
|
|
||||||
.check();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toBeHidden();
|
|
||||||
|
|
||||||
await frame
|
|
||||||
.getByRole('checkbox', {
|
|
||||||
name: 'Insecure',
|
|
||||||
})
|
|
||||||
.uncheck();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Test certificate validation', async () => {
|
|
||||||
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'File upload' })
|
|
||||||
.fill(invalidCertificate);
|
|
||||||
await expect(frame.getByText(/Certificate.*is not valid/)).toBeVisible();
|
|
||||||
|
|
||||||
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'File upload' })
|
|
||||||
.fill(validCertificate);
|
|
||||||
|
|
||||||
await expect(frame.getByText('Certificate was uploaded')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Test HTTP URL behavior', async () => {
|
|
||||||
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'ansible callback url' })
|
|
||||||
.fill(validHttpCallbackUrl);
|
|
||||||
|
|
||||||
// TLS confirmation checkbox should NOT appear for HTTP URLs
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('checkbox', {
|
|
||||||
name: 'Insecure',
|
|
||||||
}),
|
|
||||||
).toBeHidden();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'ansible callback url' })
|
|
||||||
.fill(validCallbackUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Complete AAP configuration and proceed to review', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Fill the BP details', async () => {
|
|
||||||
await fillInDetails(frame, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Create BP', async () => {
|
|
||||||
await createBlueprint(frame, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Edit BP and verify AAP configuration persists', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
|
||||||
await frame.getByLabel('Revisit Ansible Automation Platform step').click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'ansible callback url' }),
|
|
||||||
).toHaveValue(validCallbackUrl);
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'host config key' }),
|
|
||||||
).toHaveValue(validHostConfigKey);
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toHaveValue(validCertificate);
|
|
||||||
|
|
||||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
|
||||||
await frame
|
|
||||||
.getByRole('button', { name: 'Save changes to blueprint' })
|
|
||||||
.click();
|
|
||||||
});
|
|
||||||
// This is for hosted service only as these features are not available in cockpit plugin
|
|
||||||
await test.step('Export BP', async (step) => {
|
|
||||||
step.skip(!isHosted(), 'Exporting is not available in the plugin');
|
|
||||||
await exportBlueprint(page, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Import BP', async (step) => {
|
|
||||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
|
||||||
await importBlueprint(page, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Review imported BP', async (step) => {
|
|
||||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
|
||||||
await fillInImageOutputGuest(page);
|
|
||||||
await page
|
|
||||||
.getByRole('button', { name: 'Ansible Automation Platform' })
|
|
||||||
.click();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('textbox', { name: 'ansible callback url' }),
|
|
||||||
).toHaveValue(validCallbackUrl);
|
|
||||||
await expect(
|
|
||||||
page.getByRole('textbox', { name: 'host config key' }),
|
|
||||||
).toBeEmpty();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('textbox', { name: 'File upload' }),
|
|
||||||
).toHaveValue(validCertificate);
|
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { FILE_SYSTEM_CUSTOMIZATION_URL } from '../../src/constants';
|
|
||||||
import { test } from '../fixtures/cleanup';
|
|
||||||
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';
|
|
||||||
|
|
||||||
test('Create a blueprint with Filesystem customization', async ({
|
|
||||||
page,
|
|
||||||
cleanup,
|
|
||||||
}) => {
|
|
||||||
const blueprintName = 'test-' + uuidv4();
|
|
||||||
|
|
||||||
// Delete the blueprint after the run fixture
|
|
||||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
|
||||||
|
|
||||||
await ensureAuthenticated(page);
|
|
||||||
|
|
||||||
// Login, navigate to IB 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('Check URLs for documentation', async () => {
|
|
||||||
await frame
|
|
||||||
.getByRole('button', { name: 'File system configuration' })
|
|
||||||
.click();
|
|
||||||
await frame
|
|
||||||
.getByRole('radio', { name: 'Use automatic partitioning' })
|
|
||||||
.click();
|
|
||||||
const [newPageAutomatic] = await Promise.all([
|
|
||||||
page.context().waitForEvent('page'),
|
|
||||||
frame
|
|
||||||
.getByRole('link', {
|
|
||||||
name: 'Customizing file systems during the image creation',
|
|
||||||
})
|
|
||||||
.click(),
|
|
||||||
]);
|
|
||||||
await newPageAutomatic.waitForLoadState();
|
|
||||||
const finalUrlAutomatic = newPageAutomatic.url();
|
|
||||||
expect(finalUrlAutomatic).toContain(FILE_SYSTEM_CUSTOMIZATION_URL);
|
|
||||||
await newPageAutomatic.close();
|
|
||||||
|
|
||||||
await frame
|
|
||||||
.getByRole('radio', { name: 'Manually configure partitions' })
|
|
||||||
.click();
|
|
||||||
const [newPageManual] = await Promise.all([
|
|
||||||
page.context().waitForEvent('page'),
|
|
||||||
frame
|
|
||||||
.getByRole('link', {
|
|
||||||
name: 'Read more about manual configuration here',
|
|
||||||
})
|
|
||||||
.click(),
|
|
||||||
]);
|
|
||||||
await newPageManual.waitForLoadState();
|
|
||||||
const finalUrlManual = newPageManual.url();
|
|
||||||
expect(finalUrlManual).toContain(FILE_SYSTEM_CUSTOMIZATION_URL);
|
|
||||||
await newPageManual.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Fill manually selected partitions', async () => {
|
|
||||||
await expect(frame.getByRole('button', { name: '/' })).toBeDisabled();
|
|
||||||
const closeRootButton = frame
|
|
||||||
.getByRole('row', {
|
|
||||||
name: 'Draggable row draggable button / xfs 10 GiB',
|
|
||||||
})
|
|
||||||
.getByRole('button')
|
|
||||||
.nth(3);
|
|
||||||
await expect(closeRootButton).toBeDisabled();
|
|
||||||
|
|
||||||
await frame.getByRole('button', { name: 'Add partition' }).click();
|
|
||||||
await frame.getByRole('button', { name: '/home' }).click();
|
|
||||||
await frame.getByRole('option', { name: '/tmp' }).click();
|
|
||||||
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'mountpoint suffix' })
|
|
||||||
.fill('/usb');
|
|
||||||
await frame
|
|
||||||
.getByRole('gridcell', { name: '1', exact: true })
|
|
||||||
.getByPlaceholder('File system')
|
|
||||||
.fill('1000');
|
|
||||||
await frame.getByRole('button', { name: 'GiB' }).nth(1).click();
|
|
||||||
await frame.getByRole('option', { name: 'KiB' }).click();
|
|
||||||
|
|
||||||
const closeTmpButton = frame
|
|
||||||
.getByRole('row', {
|
|
||||||
name: 'Draggable row draggable button /tmp /usb xfs 1000 KiB',
|
|
||||||
})
|
|
||||||
.getByRole('button')
|
|
||||||
.nth(3);
|
|
||||||
|
|
||||||
await expect(closeTmpButton).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Fill the BP details', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
|
||||||
await fillInDetails(frame, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Create BP', async () => {
|
|
||||||
await createBlueprint(frame, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Edit BP', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
|
||||||
await frame.getByLabel('Revisit File system configuration step').click();
|
|
||||||
|
|
||||||
const closeRootButton = frame
|
|
||||||
.getByRole('row', {
|
|
||||||
name: 'Draggable row draggable button / xfs 10 GiB',
|
|
||||||
})
|
|
||||||
.getByRole('button')
|
|
||||||
.nth(3);
|
|
||||||
await expect(closeRootButton).toBeDisabled();
|
|
||||||
|
|
||||||
const closeTmpButton = frame
|
|
||||||
.getByRole('row', {
|
|
||||||
name: 'Draggable row draggable button /tmp /usb xfs 1000 KiB',
|
|
||||||
})
|
|
||||||
.getByRole('button')
|
|
||||||
.nth(3);
|
|
||||||
await expect(closeTmpButton).toBeEnabled();
|
|
||||||
|
|
||||||
const usbTextbox = frame.getByRole('textbox', {
|
|
||||||
name: 'mountpoint suffix',
|
|
||||||
});
|
|
||||||
await expect(usbTextbox).toHaveValue('/usb');
|
|
||||||
|
|
||||||
await frame
|
|
||||||
.getByRole('gridcell', { name: '1000', exact: true })
|
|
||||||
.getByPlaceholder('File system')
|
|
||||||
.click();
|
|
||||||
await frame
|
|
||||||
.getByRole('gridcell', { name: '1000', exact: true })
|
|
||||||
.getByPlaceholder('File system')
|
|
||||||
.fill('1024');
|
|
||||||
|
|
||||||
await frame.getByRole('button', { name: '/tmp' }).click();
|
|
||||||
await frame.getByRole('option', { name: '/usr' }).click();
|
|
||||||
await expect(
|
|
||||||
frame.getByText(
|
|
||||||
'Sub-directories for the /usr mount point are no longer supported',
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await frame.getByRole('button', { name: '/usr' }).click();
|
|
||||||
await frame.getByRole('option', { name: '/srv' }).click();
|
|
||||||
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'mountpoint suffix' })
|
|
||||||
.fill('/data');
|
|
||||||
|
|
||||||
await frame.getByRole('button', { name: 'KiB' }).click();
|
|
||||||
await frame.getByRole('option', { name: 'MiB' }).click();
|
|
||||||
|
|
||||||
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 frame
|
|
||||||
.getByRole('button', { name: 'File system configuration' })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const closeRootButton = frame
|
|
||||||
.getByRole('row', {
|
|
||||||
name: 'Draggable row draggable button / xfs 10 GiB',
|
|
||||||
})
|
|
||||||
.getByRole('button')
|
|
||||||
.nth(3);
|
|
||||||
await expect(closeRootButton).toBeDisabled();
|
|
||||||
|
|
||||||
const closeTmpButton = frame
|
|
||||||
.getByRole('row', {
|
|
||||||
name: 'Draggable row draggable button /srv /data xfs 1 GiB',
|
|
||||||
})
|
|
||||||
.getByRole('button')
|
|
||||||
.nth(3);
|
|
||||||
await expect(closeTmpButton).toBeEnabled();
|
|
||||||
|
|
||||||
const dataTextbox = frame.getByRole('textbox', {
|
|
||||||
name: 'mountpoint suffix',
|
|
||||||
});
|
|
||||||
await expect(dataTextbox).toHaveValue('/data');
|
|
||||||
|
|
||||||
const size = frame
|
|
||||||
.getByRole('gridcell', { name: '1', exact: true })
|
|
||||||
.getByPlaceholder('File system');
|
|
||||||
await expect(size).toHaveValue('1');
|
|
||||||
|
|
||||||
const unitButton = frame.getByRole('button', { name: 'GiB' }).nth(1);
|
|
||||||
await expect(unitButton).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { test } from '../fixtures/customizations';
|
import { test } from '../fixtures/cleanup';
|
||||||
import { isHosted } from '../helpers/helpers';
|
import { isHosted } from '../helpers/helpers';
|
||||||
import { ensureAuthenticated } from '../helpers/login';
|
import { login } from '../helpers/login';
|
||||||
import {
|
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||||
ibFrame,
|
|
||||||
navigateToLandingPage,
|
|
||||||
navigateToOptionalSteps,
|
|
||||||
} from '../helpers/navHelpers';
|
|
||||||
import {
|
import {
|
||||||
|
registerLater,
|
||||||
|
fillInDetails,
|
||||||
createBlueprint,
|
createBlueprint,
|
||||||
|
fillInImageOutputGuest,
|
||||||
deleteBlueprint,
|
deleteBlueprint,
|
||||||
exportBlueprint,
|
exportBlueprint,
|
||||||
fillInDetails,
|
|
||||||
fillInImageOutputGuest,
|
|
||||||
importBlueprint,
|
importBlueprint,
|
||||||
registerLater,
|
|
||||||
} from '../helpers/wizardHelpers';
|
} from '../helpers/wizardHelpers';
|
||||||
|
|
||||||
test('Create a blueprint with Firewall customization', async ({
|
test('Create a blueprint with Firewall customization', async ({
|
||||||
|
|
@ -28,10 +24,8 @@ test('Create a blueprint with Firewall customization', async ({
|
||||||
// Delete the blueprint after the run fixture
|
// Delete the blueprint after the run fixture
|
||||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||||
|
|
||||||
await ensureAuthenticated(page);
|
// Login, navigate to IB and get the frame
|
||||||
|
await login(page);
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
|
|
||||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
await test.step('Navigate to optional steps in Wizard', async () => {
|
||||||
|
|
@ -63,29 +57,19 @@ test('Create a blueprint with Firewall customization', async ({
|
||||||
await test.step('Select and incorrectly fill the ports in Firewall step', async () => {
|
await test.step('Select and incorrectly fill the ports in Firewall step', async () => {
|
||||||
await frame.getByPlaceholder('Add ports').fill('x');
|
await frame.getByPlaceholder('Add ports').fill('x');
|
||||||
await frame.getByRole('button', { name: 'Add ports' }).click();
|
await frame.getByRole('button', { name: 'Add ports' }).click();
|
||||||
await expect(
|
await expect(frame.getByText('Invalid format.').nth(0)).toBeVisible();
|
||||||
frame
|
|
||||||
.getByText(
|
|
||||||
'Expected format: <port/port-name>:<protocol>. Example: 8080:tcp, ssh:tcp',
|
|
||||||
)
|
|
||||||
.nth(0),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Select and incorrectly fill the disabled services in Firewall step', async () => {
|
await test.step('Select and incorrectly fill the disabled services in Firewall step', async () => {
|
||||||
await frame.getByPlaceholder('Add disabled service').fill('1');
|
await frame.getByPlaceholder('Add disabled service').fill('1');
|
||||||
await frame.getByRole('button', { name: 'Add disabled service' }).click();
|
await frame.getByRole('button', { name: 'Add disabled service' }).click();
|
||||||
await expect(
|
await expect(frame.getByText('Invalid format.').nth(1)).toBeVisible();
|
||||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(0),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Select and incorrectly fill the enabled services in Firewall step', async () => {
|
await test.step('Select and incorrectly fill the enabled services in Firewall step', async () => {
|
||||||
await frame.getByPlaceholder('Add enabled service').fill('ťčš');
|
await frame.getByPlaceholder('Add enabled service').fill('ťčš');
|
||||||
await frame.getByRole('button', { name: 'Add enabled service' }).click();
|
await frame.getByRole('button', { name: 'Add enabled service' }).click();
|
||||||
await expect(
|
await expect(frame.getByText('Invalid format.').nth(2)).toBeVisible();
|
||||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(1),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Fill the BP details', async () => {
|
await test.step('Fill the BP details', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { test } from '../fixtures/customizations';
|
import { test } from '../fixtures/cleanup';
|
||||||
import { isHosted } from '../helpers/helpers';
|
import { isHosted } from '../helpers/helpers';
|
||||||
import { ensureAuthenticated } from '../helpers/login';
|
import { login } from '../helpers/login';
|
||||||
import {
|
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||||
ibFrame,
|
|
||||||
navigateToLandingPage,
|
|
||||||
navigateToOptionalSteps,
|
|
||||||
} from '../helpers/navHelpers';
|
|
||||||
import {
|
import {
|
||||||
|
registerLater,
|
||||||
|
fillInDetails,
|
||||||
createBlueprint,
|
createBlueprint,
|
||||||
|
fillInImageOutputGuest,
|
||||||
deleteBlueprint,
|
deleteBlueprint,
|
||||||
exportBlueprint,
|
exportBlueprint,
|
||||||
fillInDetails,
|
|
||||||
fillInImageOutputGuest,
|
|
||||||
importBlueprint,
|
importBlueprint,
|
||||||
registerLater,
|
|
||||||
} from '../helpers/wizardHelpers';
|
} from '../helpers/wizardHelpers';
|
||||||
|
|
||||||
test('Create a blueprint with Hostname customization', async ({
|
test('Create a blueprint with Hostname customization', async ({
|
||||||
|
|
@ -29,10 +25,8 @@ test('Create a blueprint with Hostname customization', async ({
|
||||||
// Delete the blueprint after the run fixture
|
// Delete the blueprint after the run fixture
|
||||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||||
|
|
||||||
await ensureAuthenticated(page);
|
// Login, navigate to IB and get the frame
|
||||||
|
await login(page);
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
|
|
||||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
await test.step('Navigate to optional steps in Wizard', async () => {
|
||||||
|
|
@ -83,7 +77,7 @@ test('Create a blueprint with Hostname customization', async ({
|
||||||
await fillInImageOutputGuest(page);
|
await fillInImageOutputGuest(page);
|
||||||
await page.getByRole('button', { name: 'Hostname' }).click();
|
await page.getByRole('button', { name: 'Hostname' }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('textbox', { name: 'hostname input' }),
|
page.getByRole('textbox', { name: 'hostname input' })
|
||||||
).toHaveValue(hostname + 'edited');
|
).toHaveValue(hostname + 'edited');
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { test } from '../fixtures/customizations';
|
|
||||||
import { isHosted } from '../helpers/helpers';
|
|
||||||
import { ensureAuthenticated } from '../helpers/login';
|
|
||||||
import {
|
|
||||||
ibFrame,
|
|
||||||
navigateToLandingPage,
|
|
||||||
navigateToOptionalSteps,
|
|
||||||
} from '../helpers/navHelpers';
|
|
||||||
import {
|
|
||||||
createBlueprint,
|
|
||||||
deleteBlueprint,
|
|
||||||
exportBlueprint,
|
|
||||||
fillInDetails,
|
|
||||||
fillInImageOutputGuest,
|
|
||||||
importBlueprint,
|
|
||||||
registerLater,
|
|
||||||
} from '../helpers/wizardHelpers';
|
|
||||||
|
|
||||||
test('Create a blueprint with Kernel customization', async ({
|
|
||||||
page,
|
|
||||||
cleanup,
|
|
||||||
}) => {
|
|
||||||
const blueprintName = 'test-' + uuidv4();
|
|
||||||
|
|
||||||
// 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 Kernel step', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Kernel' }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Menu toggle' }).click();
|
|
||||||
await frame.getByRole('option', { name: 'kernel', exact: true }).click();
|
|
||||||
await frame.getByPlaceholder('Add kernel argument').fill('rootwait');
|
|
||||||
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
|
|
||||||
await frame
|
|
||||||
.getByPlaceholder('Add kernel argument')
|
|
||||||
.fill('invalid$argument');
|
|
||||||
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
|
|
||||||
await expect(
|
|
||||||
frame.getByText(
|
|
||||||
'Expected format: <kernel-argument>. Example: console=tty0',
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByPlaceholder('Select kernel package').fill('new-package');
|
|
||||||
await frame
|
|
||||||
.getByRole('option', { name: 'Custom kernel package "new-' })
|
|
||||||
.click();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('heading', { name: 'Warning alert: Custom kernel' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByRole('button', { name: 'Clear input' }).first().click();
|
|
||||||
await frame.getByRole('button', { name: 'Menu toggle' }).click();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('option', { name: 'new-package' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByPlaceholder('Select kernel package').fill('f');
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('option', {
|
|
||||||
name: '"f" is not a valid kernel package name',
|
|
||||||
}),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByPlaceholder('Add kernel argument').fill('console=tty0');
|
|
||||||
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
|
|
||||||
await frame.getByPlaceholder('Add kernel argument').fill('xxnosmp');
|
|
||||||
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
|
|
||||||
await frame
|
|
||||||
.getByPlaceholder('Add kernel argument')
|
|
||||||
.fill('console=ttyS0,115200n8');
|
|
||||||
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
|
|
||||||
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', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
|
||||||
await frame.getByLabel('Revisit Kernel step').click();
|
|
||||||
await frame.getByRole('button', { name: 'Menu toggle' }).click();
|
|
||||||
await frame.getByRole('option', { name: 'kernel', exact: true }).click();
|
|
||||||
await frame.getByPlaceholder('Add kernel argument').fill('new=argument');
|
|
||||||
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Close xxnosmp' }).click();
|
|
||||||
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(frame);
|
|
||||||
await frame.getByRole('button', { name: 'Kernel' }).click();
|
|
||||||
await expect(frame.getByPlaceholder('Select kernel package')).toHaveValue(
|
|
||||||
'kernel',
|
|
||||||
);
|
|
||||||
await expect(frame.getByText('rootwait')).toBeVisible();
|
|
||||||
await expect(frame.getByText('console=tty0')).toBeVisible();
|
|
||||||
await expect(frame.getByText('console=ttyS0,115200n8')).toBeVisible();
|
|
||||||
await expect(frame.getByText('new=argument')).toBeVisible();
|
|
||||||
await expect(frame.getByText('xxnosmp')).toBeHidden();
|
|
||||||
await frame.getByRole('button', { name: 'Cancel' }).click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { test } from '../fixtures/customizations';
|
import { test } from '../fixtures/cleanup';
|
||||||
import { isHosted } from '../helpers/helpers';
|
import { isHosted } from '../helpers/helpers';
|
||||||
import { ensureAuthenticated } from '../helpers/login';
|
import { login } from '../helpers/login';
|
||||||
import {
|
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||||
ibFrame,
|
|
||||||
navigateToLandingPage,
|
|
||||||
navigateToOptionalSteps,
|
|
||||||
} from '../helpers/navHelpers';
|
|
||||||
import {
|
import {
|
||||||
|
registerLater,
|
||||||
|
fillInDetails,
|
||||||
createBlueprint,
|
createBlueprint,
|
||||||
|
fillInImageOutputGuest,
|
||||||
deleteBlueprint,
|
deleteBlueprint,
|
||||||
exportBlueprint,
|
exportBlueprint,
|
||||||
fillInDetails,
|
|
||||||
fillInImageOutputGuest,
|
|
||||||
importBlueprint,
|
importBlueprint,
|
||||||
registerLater,
|
|
||||||
} from '../helpers/wizardHelpers';
|
} from '../helpers/wizardHelpers';
|
||||||
|
|
||||||
test('Create a blueprint with Locale customization', async ({
|
test('Create a blueprint with Locale customization', async ({
|
||||||
|
|
@ -28,10 +24,8 @@ test('Create a blueprint with Locale customization', async ({
|
||||||
// Delete the blueprint after the run fixture
|
// Delete the blueprint after the run fixture
|
||||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||||
|
|
||||||
await ensureAuthenticated(page);
|
// Login, navigate to IB and get the frame
|
||||||
|
await login(page);
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
|
|
||||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
await test.step('Navigate to optional steps in Wizard', async () => {
|
||||||
|
|
@ -42,45 +36,27 @@ test('Create a blueprint with Locale customization', async ({
|
||||||
await test.step('Select and fill the Locale step', async () => {
|
await test.step('Select and fill the Locale step', async () => {
|
||||||
await frame.getByRole('button', { name: 'Locale' }).click();
|
await frame.getByRole('button', { name: 'Locale' }).click();
|
||||||
await frame.getByPlaceholder('Select a language').fill('fy');
|
await frame.getByPlaceholder('Select a language').fill('fy');
|
||||||
await frame
|
await frame.getByRole('option', { name: 'fy_DE.UTF-' }).click();
|
||||||
.getByRole('option', { name: 'Western Frisian - Germany (fy_DE.UTF-8)' })
|
|
||||||
.click();
|
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', {
|
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
|
||||||
}),
|
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await frame
|
await frame.getByRole('button', { name: 'Close fy_DE.UTF-' }).click();
|
||||||
.getByRole('button', {
|
|
||||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', {
|
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
|
||||||
}),
|
|
||||||
).toBeHidden();
|
).toBeHidden();
|
||||||
await frame.getByPlaceholder('Select a language').fill('fy');
|
await frame.getByPlaceholder('Select a language').fill('fy');
|
||||||
await frame
|
await frame.getByRole('option', { name: 'fy_DE.UTF-' }).click();
|
||||||
.getByRole('option', { name: 'Western Frisian - Germany (fy_DE.UTF-8)' })
|
|
||||||
.click();
|
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', {
|
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
|
||||||
}),
|
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await frame.getByPlaceholder('Select a language').fill('aa');
|
await frame.getByPlaceholder('Select a language').fill('aa');
|
||||||
await frame
|
await frame.getByRole('option', { name: 'aa_DJ.UTF-' }).click();
|
||||||
.getByRole('option', { name: 'aa - Djibouti (aa_DJ.UTF-8)' })
|
|
||||||
.click();
|
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
|
frame.getByRole('button', { name: 'Close aa_DJ.UTF-' })
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await frame.getByPlaceholder('Select a language').fill('aa');
|
await frame.getByPlaceholder('Select a language').fill('aa');
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByText(
|
frame.getByText('aa_DJ.UTF-8Language already addedaa_ER.UTF-8aa_ET.UTF-')
|
||||||
'aa - Djibouti (aa_DJ.UTF-8)Language already addedaa - Eritrea (aa_ER.UTF-8)aa - Ethiopia (aa_ET.UTF-8)',
|
|
||||||
),
|
|
||||||
).toBeAttached();
|
).toBeAttached();
|
||||||
await frame.getByPlaceholder('Select a language').fill('xxx');
|
await frame.getByPlaceholder('Select a language').fill('xxx');
|
||||||
await expect(frame.getByText('No results found for')).toBeAttached();
|
await expect(frame.getByText('No results found for')).toBeAttached();
|
||||||
|
|
@ -102,19 +78,15 @@ test('Create a blueprint with Locale customization', async ({
|
||||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
||||||
await frame.getByLabel('Revisit Locale step').click();
|
await frame.getByLabel('Revisit Locale step').click();
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', {
|
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
|
||||||
}),
|
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
|
frame.getByRole('button', { name: 'Close aa_DJ.UTF-' })
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await frame.getByPlaceholder('Select a language').fill('aa');
|
await frame.getByPlaceholder('Select a language').fill('aa');
|
||||||
await frame
|
await frame.getByRole('option', { name: 'aa_ER.UTF-' }).click();
|
||||||
.getByRole('option', { name: 'aa - Eritrea (aa_ER.UTF-8)' })
|
|
||||||
.click();
|
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', { name: 'Close aa - Eritrea (aa_ER.UTF-8)' }),
|
frame.getByRole('button', { name: 'Close aa_ER.UTF-' })
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await frame.getByRole('button', { name: 'Clear input' }).click();
|
await frame.getByRole('button', { name: 'Clear input' }).click();
|
||||||
await frame.getByRole('button', { name: 'Menu toggle' }).nth(1).click();
|
await frame.getByRole('button', { name: 'Menu toggle' }).nth(1).click();
|
||||||
|
|
@ -141,18 +113,16 @@ test('Create a blueprint with Locale customization', async ({
|
||||||
await fillInImageOutputGuest(page);
|
await fillInImageOutputGuest(page);
|
||||||
await page.getByRole('button', { name: 'Locale' }).click();
|
await page.getByRole('button', { name: 'Locale' }).click();
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', {
|
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
|
||||||
}),
|
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
|
frame.getByRole('button', { name: 'Close aa_DJ.UTF-' })
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('button', { name: 'Close aa - Eritrea (aa_ER.UTF-8)' }),
|
frame.getByRole('button', { name: 'Close aa_ER.UTF-' })
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await expect(frame.getByPlaceholder('Select a keyboard')).toHaveValue(
|
await expect(frame.getByPlaceholder('Select a keyboard')).toHaveValue(
|
||||||
'ANSI-dvorak',
|
'ANSI-dvorak'
|
||||||
);
|
);
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { test } from '../fixtures/cleanup';
|
|
||||||
import { isHosted } from '../helpers/helpers';
|
|
||||||
import { ensureAuthenticated } from '../helpers/login';
|
|
||||||
import { ibFrame, navigateToLandingPage } from '../helpers/navHelpers';
|
|
||||||
import {
|
|
||||||
createBlueprint,
|
|
||||||
deleteBlueprint,
|
|
||||||
exportBlueprint,
|
|
||||||
fillInDetails,
|
|
||||||
fillInImageOutputGuest,
|
|
||||||
importBlueprint,
|
|
||||||
registerLater,
|
|
||||||
} from '../helpers/wizardHelpers';
|
|
||||||
|
|
||||||
test('Create a blueprint with OpenSCAP customization', async ({
|
|
||||||
page,
|
|
||||||
cleanup,
|
|
||||||
}) => {
|
|
||||||
const blueprintName = 'test-' + uuidv4();
|
|
||||||
test.skip(!isHosted(), 'Exporting 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('Select RHEL 9 and go to optional steps in Wizard', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Create image blueprint' }).click();
|
|
||||||
await frame.getByTestId('release_select').click();
|
|
||||||
await frame
|
|
||||||
.getByRole('option', {
|
|
||||||
name: 'Red Hat Enterprise Linux (RHEL) 9 Full support ends: May 2027 | Maintenance',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
await frame.getByRole('checkbox', { name: 'Virtualization' }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Next' }).click();
|
|
||||||
await registerLater(frame);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Select only OpenSCAP, and check if dependencies are preselected', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Compliance' }).click();
|
|
||||||
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
|
|
||||||
await frame
|
|
||||||
.getByRole('option', {
|
|
||||||
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server This profile',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
await frame
|
|
||||||
.getByRole('button', { name: 'File system configuration' })
|
|
||||||
.click();
|
|
||||||
await expect(
|
|
||||||
frame
|
|
||||||
.getByRole('row', {
|
|
||||||
name: 'Draggable row draggable button /tmp xfs 1 GiB',
|
|
||||||
})
|
|
||||||
.getByRole('button')
|
|
||||||
.nth(3),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByRole('button', { name: 'Additional packages' }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Selected (8)' }).click();
|
|
||||||
await expect(frame.getByRole('gridcell', { name: 'aide' })).toBeVisible();
|
|
||||||
await expect(frame.getByRole('gridcell', { name: 'chrony' })).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'firewalld' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'libpwquality' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'libselinux' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'nftables' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(frame.getByRole('gridcell', { name: 'sudo' })).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'systemd-journal-remote' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByRole('button', { name: 'Systemd services' }).click();
|
|
||||||
await expect(
|
|
||||||
frame.getByText('Required by OpenSCAPcrondfirewalldsystemd-journald'),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByPlaceholder('Add masked service').fill('nftables');
|
|
||||||
await frame.getByPlaceholder('Add masked service').press('Enter');
|
|
||||||
await expect(
|
|
||||||
frame.getByText('Masked service already exists'),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(frame.getByText('Required by OpenSCAPcupsnfs-')).toBeVisible();
|
|
||||||
await expect(frame.getByText('nfs-server')).toBeVisible();
|
|
||||||
await expect(frame.getByText('rpcbind')).toBeVisible();
|
|
||||||
await expect(frame.getByText('avahi-daemon')).toBeVisible();
|
|
||||||
await expect(frame.getByText('autofs')).toBeVisible();
|
|
||||||
await expect(frame.getByText('bluetooth')).toBeVisible();
|
|
||||||
await expect(frame.getByText('nftables')).toBeVisible();
|
|
||||||
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', async () => {
|
|
||||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Compliance' }).click();
|
|
||||||
await expect(frame.getByText('Level 1 - Server')).toBeVisible();
|
|
||||||
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
|
|
||||||
await frame
|
|
||||||
.getByRole('option', {
|
|
||||||
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 2 - Server This profile',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await frame.getByRole('button', { name: 'Kernel' }).click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
frame.getByText('Required by OpenSCAPaudit_backlog_limit=8192audit='),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByRole('button', { name: 'Additional packages' }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Selected (10)' }).click();
|
|
||||||
await expect(frame.getByRole('gridcell', { name: 'aide' })).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'audit-libs' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(frame.getByRole('gridcell', { name: 'chrony' })).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'firewalld' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'libpwquality' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'libselinux' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('gridcell', { name: 'nftables' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(frame.getByRole('gridcell', { name: 'sudo' })).toBeVisible();
|
|
||||||
await frame.getByRole('button', { name: 'Systemd services' }).click();
|
|
||||||
await expect(
|
|
||||||
frame.getByText(
|
|
||||||
'Required by OpenSCAPauditdcrondfirewalldsystemd-journald',
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByPlaceholder('Add masked service').fill('nftables');
|
|
||||||
await frame.getByPlaceholder('Add masked service').press('Enter');
|
|
||||||
await expect(
|
|
||||||
frame.getByText('Masked service already exists'),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(frame.getByText('Required by OpenSCAPcupsnfs-')).toBeVisible();
|
|
||||||
await expect(frame.getByText('nfs-server')).toBeVisible();
|
|
||||||
await expect(frame.getByText('rpcbind')).toBeVisible();
|
|
||||||
await expect(frame.getByText('avahi-daemon')).toBeVisible();
|
|
||||||
await expect(frame.getByText('autofs')).toBeVisible();
|
|
||||||
await expect(frame.getByText('bluetooth')).toBeVisible();
|
|
||||||
await expect(frame.getByText('nftables')).toBeVisible();
|
|
||||||
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 () => {
|
|
||||||
await exportBlueprint(page, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Import BP', async () => {
|
|
||||||
await importBlueprint(page, blueprintName);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Review imported BP', async () => {
|
|
||||||
await fillInImageOutputGuest(page);
|
|
||||||
await page.getByRole('button', { name: 'Compliance' }).click();
|
|
||||||
|
|
||||||
await expect(frame.getByText('Level 2 - Server')).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { test } from '../fixtures/customizations';
|
import { test } from '../fixtures/cleanup';
|
||||||
import { isHosted } from '../helpers/helpers';
|
import { isHosted } from '../helpers/helpers';
|
||||||
import { ensureAuthenticated } from '../helpers/login';
|
import { login } from '../helpers/login';
|
||||||
import {
|
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||||
ibFrame,
|
|
||||||
navigateToLandingPage,
|
|
||||||
navigateToOptionalSteps,
|
|
||||||
} from '../helpers/navHelpers';
|
|
||||||
import {
|
import {
|
||||||
|
registerLater,
|
||||||
|
fillInDetails,
|
||||||
createBlueprint,
|
createBlueprint,
|
||||||
|
fillInImageOutputGuest,
|
||||||
deleteBlueprint,
|
deleteBlueprint,
|
||||||
exportBlueprint,
|
exportBlueprint,
|
||||||
fillInDetails,
|
|
||||||
fillInImageOutputGuest,
|
|
||||||
importBlueprint,
|
importBlueprint,
|
||||||
registerLater,
|
|
||||||
} from '../helpers/wizardHelpers';
|
} from '../helpers/wizardHelpers';
|
||||||
|
|
||||||
test('Create a blueprint with Systemd customization', async ({
|
test('Create a blueprint with Systemd customization', async ({
|
||||||
|
|
@ -28,10 +24,8 @@ test('Create a blueprint with Systemd customization', async ({
|
||||||
// Delete the blueprint after the run fixture
|
// Delete the blueprint after the run fixture
|
||||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||||
|
|
||||||
await ensureAuthenticated(page);
|
// Login, navigate to IB and get the frame
|
||||||
|
await login(page);
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
|
|
||||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
await test.step('Navigate to optional steps in Wizard', async () => {
|
||||||
|
|
@ -64,21 +58,15 @@ test('Create a blueprint with Systemd customization', async ({
|
||||||
await test.step('Select and incorrectly fill all of the service fields', async () => {
|
await test.step('Select and incorrectly fill all of the service fields', async () => {
|
||||||
await frame.getByPlaceholder('Add disabled service').fill('&&');
|
await frame.getByPlaceholder('Add disabled service').fill('&&');
|
||||||
await frame.getByRole('button', { name: 'Add disabled service' }).click();
|
await frame.getByRole('button', { name: 'Add disabled service' }).click();
|
||||||
await expect(
|
await expect(frame.getByText('Invalid format.').nth(0)).toBeVisible();
|
||||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(0),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await frame.getByPlaceholder('Add enabled service').fill('áá');
|
await frame.getByPlaceholder('Add enabled service').fill('áá');
|
||||||
await frame.getByRole('button', { name: 'Add enabled service' }).click();
|
await frame.getByRole('button', { name: 'Add enabled service' }).click();
|
||||||
await expect(
|
await expect(frame.getByText('Invalid format.').nth(1)).toBeVisible();
|
||||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(1),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await frame.getByPlaceholder('Add masked service').fill('78');
|
await frame.getByPlaceholder('Add masked service').fill('78');
|
||||||
await frame.getByRole('button', { name: 'Add masked service' }).click();
|
await frame.getByRole('button', { name: 'Add masked service' }).click();
|
||||||
await expect(
|
await expect(frame.getByText('Invalid format.').nth(2)).toBeVisible();
|
||||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(2),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Fill the BP details', async () => {
|
await test.step('Fill the BP details', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { test } from '../fixtures/customizations';
|
import { test } from '../fixtures/cleanup';
|
||||||
import { isHosted } from '../helpers/helpers';
|
import { isHosted } from '../helpers/helpers';
|
||||||
import { ensureAuthenticated } from '../helpers/login';
|
import { login } from '../helpers/login';
|
||||||
import {
|
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||||
ibFrame,
|
|
||||||
navigateToLandingPage,
|
|
||||||
navigateToOptionalSteps,
|
|
||||||
} from '../helpers/navHelpers';
|
|
||||||
import {
|
import {
|
||||||
|
registerLater,
|
||||||
|
fillInDetails,
|
||||||
createBlueprint,
|
createBlueprint,
|
||||||
|
fillInImageOutputGuest,
|
||||||
deleteBlueprint,
|
deleteBlueprint,
|
||||||
exportBlueprint,
|
exportBlueprint,
|
||||||
fillInDetails,
|
|
||||||
fillInImageOutputGuest,
|
|
||||||
importBlueprint,
|
importBlueprint,
|
||||||
registerLater,
|
|
||||||
} from '../helpers/wizardHelpers';
|
} from '../helpers/wizardHelpers';
|
||||||
|
|
||||||
test('Create a blueprint with Timezone customization', async ({
|
test('Create a blueprint with Timezone customization', async ({
|
||||||
|
|
@ -28,10 +24,8 @@ test('Create a blueprint with Timezone customization', async ({
|
||||||
// Delete the blueprint after the run fixture
|
// Delete the blueprint after the run fixture
|
||||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||||
|
|
||||||
await ensureAuthenticated(page);
|
// Login, navigate to IB and get the frame
|
||||||
|
await login(page);
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
|
|
||||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
await test.step('Navigate to optional steps in Wizard', async () => {
|
||||||
|
|
@ -55,11 +49,7 @@ test('Create a blueprint with Timezone customization', async ({
|
||||||
await expect(frame.getByText('NTP server already exists.')).toBeVisible();
|
await expect(frame.getByText('NTP server already exists.')).toBeVisible();
|
||||||
await frame.getByPlaceholder('Add NTP servers').fill('xxxx');
|
await frame.getByPlaceholder('Add NTP servers').fill('xxxx');
|
||||||
await frame.getByRole('button', { name: 'Add NTP server' }).click();
|
await frame.getByRole('button', { name: 'Add NTP server' }).click();
|
||||||
await expect(
|
await expect(frame.getByText('Invalid format.')).toBeVisible();
|
||||||
frame
|
|
||||||
.getByText('Expected format: <ntp-server>. Example: time.redhat.com')
|
|
||||||
.nth(0),
|
|
||||||
).toBeVisible();
|
|
||||||
await frame.getByPlaceholder('Add NTP servers').fill('0.cz.pool.ntp.org');
|
await frame.getByPlaceholder('Add NTP servers').fill('0.cz.pool.ntp.org');
|
||||||
await frame.getByRole('button', { name: 'Add NTP server' }).click();
|
await frame.getByRole('button', { name: 'Add NTP server' }).click();
|
||||||
await expect(frame.getByText('0.cz.pool.ntp.org')).toBeVisible();
|
await expect(frame.getByText('0.cz.pool.ntp.org')).toBeVisible();
|
||||||
|
|
@ -86,12 +76,12 @@ test('Create a blueprint with Timezone customization', async ({
|
||||||
await frame.getByLabel('Revisit Timezone step').click();
|
await frame.getByLabel('Revisit Timezone step').click();
|
||||||
await expect(frame.getByText('Canada/Saskatchewan')).toBeHidden();
|
await expect(frame.getByText('Canada/Saskatchewan')).toBeHidden();
|
||||||
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
|
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
|
||||||
'Europe/Stockholm',
|
'Europe/Stockholm'
|
||||||
);
|
);
|
||||||
await frame.getByPlaceholder('Select a timezone').fill('Europe');
|
await frame.getByPlaceholder('Select a timezone').fill('Europe');
|
||||||
await frame.getByRole('option', { name: 'Europe/Oslo' }).click();
|
await frame.getByRole('option', { name: 'Europe/Oslo' }).click();
|
||||||
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
|
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
|
||||||
'Europe/Oslo',
|
'Europe/Oslo'
|
||||||
);
|
);
|
||||||
await expect(frame.getByText('0.nl.pool.ntp.org')).toBeVisible();
|
await expect(frame.getByText('0.nl.pool.ntp.org')).toBeVisible();
|
||||||
await expect(frame.getByText('0.de.pool.ntp.org')).toBeVisible();
|
await expect(frame.getByText('0.de.pool.ntp.org')).toBeVisible();
|
||||||
|
|
@ -118,7 +108,7 @@ test('Create a blueprint with Timezone customization', async ({
|
||||||
await fillInImageOutputGuest(page);
|
await fillInImageOutputGuest(page);
|
||||||
await frame.getByRole('button', { name: 'Timezone' }).click();
|
await frame.getByRole('button', { name: 'Timezone' }).click();
|
||||||
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
|
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
|
||||||
'Europe/Oslo',
|
'Europe/Oslo'
|
||||||
);
|
);
|
||||||
await expect(frame.getByText('0.nl.pool.ntp.org')).toBeVisible();
|
await expect(frame.getByText('0.nl.pool.ntp.org')).toBeVisible();
|
||||||
await expect(frame.getByText('0.de.pool.ntp.org')).toBeVisible();
|
await expect(frame.getByText('0.de.pool.ntp.org')).toBeVisible();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export interface Cleanup {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const test = oldTest.extend<WithCleanup>({
|
export const test = oldTest.extend<WithCleanup>({
|
||||||
|
// eslint-disable-next-line no-empty-pattern
|
||||||
cleanup: async ({}, use) => {
|
cleanup: async ({}, use) => {
|
||||||
const cleanupFns: Map<symbol, () => Promise<unknown>> = new Map();
|
const cleanupFns: Map<symbol, () => Promise<unknown>> = new Map();
|
||||||
|
|
||||||
|
|
@ -38,7 +39,7 @@ export const test = oldTest.extend<WithCleanup>({
|
||||||
async () => {
|
async () => {
|
||||||
await Promise.all(Array.from(cleanupFns).map(([, fn]) => fn()));
|
await Promise.all(Array.from(cleanupFns).map(([, fn]) => fn()));
|
||||||
},
|
},
|
||||||
{ box: true },
|
{ box: true }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
// This is a common fixture for the customizations tests
|
|
||||||
import { mergeTests } from '@playwright/test';
|
|
||||||
|
|
||||||
import { test as cleanupTest } from './cleanup';
|
|
||||||
import { test as popupTest } from './popupHandler';
|
|
||||||
|
|
||||||
// Combine the fixtures into one
|
|
||||||
export const test = mergeTests(cleanupTest, popupTest);
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { test as base } from '@playwright/test';
|
|
||||||
|
|
||||||
import { closePopupsIfExist } from '../helpers/helpers';
|
|
||||||
|
|
||||||
export interface PopupHandlerFixture {
|
|
||||||
popupHandler: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This fixture will close any popups that might get opened during the test execution
|
|
||||||
export const test = base.extend<PopupHandlerFixture>({
|
|
||||||
popupHandler: [
|
|
||||||
async ({ page }, use) => {
|
|
||||||
await closePopupsIfExist(page);
|
|
||||||
await use(undefined);
|
|
||||||
},
|
|
||||||
{ auto: true },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { test as setup } from '@playwright/test';
|
|
||||||
|
|
||||||
import { login, storeStorageStateAndToken } from './helpers/login';
|
|
||||||
|
|
||||||
setup.describe('Setup', () => {
|
|
||||||
setup.describe.configure({ retries: 3 });
|
|
||||||
|
|
||||||
setup('Authenticate', async ({ page }) => {
|
|
||||||
await login(page);
|
|
||||||
await storeStorageStateAndToken(page);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import { execSync } from 'child_process';
|
import { type Page, expect } from '@playwright/test';
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
|
|
||||||
import { expect, type Page } from '@playwright/test';
|
|
||||||
|
|
||||||
export const togglePreview = async (page: Page) => {
|
export const togglePreview = async (page: Page) => {
|
||||||
const toggleSwitch = page.locator('#preview-toggle');
|
const toggleSwitch = page.locator('#preview-toggle');
|
||||||
|
|
@ -24,64 +21,19 @@ export const isHosted = (): boolean => {
|
||||||
|
|
||||||
export const closePopupsIfExist = async (page: Page) => {
|
export const closePopupsIfExist = async (page: Page) => {
|
||||||
const locatorsToCheck = [
|
const locatorsToCheck = [
|
||||||
page.locator('.pf-v6-c-alert.notification-item button'), // This closes all toast pop-ups
|
page.locator('.pf-v5-c-alert.notification-item button'), // This closes all toast pop-ups
|
||||||
page.locator(`button[id^="pendo-close-guide-"]`), // This closes the pendo guide pop-up
|
page.locator(`button[id^="pendo-close-guide-"]`), // This closes the pendo guide pop-up
|
||||||
page.locator(`button[id="truste-consent-button"]`), // This closes the trusted consent pop-up
|
page.locator(`button[id="truste-consent-button"]`), // This closes the trusted consent pop-up
|
||||||
page.getByLabel('close-notification'), // This closes a one off info notification (May be covered by the toast above, needs recheck.)
|
page.getByLabel('close-notification'), // This closes a one off info notification (May be covered by the toast above, needs recheck.)
|
||||||
page
|
page
|
||||||
.locator('iframe[name="intercom-modal-frame"]')
|
.locator('iframe[name="intercom-modal-frame"]')
|
||||||
.contentFrame()
|
.contentFrame()
|
||||||
.getByRole('button', { name: 'Close' }), // This closes the intercom pop-up
|
.getByRole('button', { name: 'Close' }),
|
||||||
page
|
|
||||||
.locator('iframe[name="intercom-notifications-frame"]')
|
|
||||||
.contentFrame()
|
|
||||||
.getByRole('button', { name: 'Profile image for Rob Rob' })
|
|
||||||
.last(), // This closes the intercom pop-up notification at the bottom of the screen, the last notification is displayed first if stacked (different from the modal popup handled above)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const locator of locatorsToCheck) {
|
for (const locator of locatorsToCheck) {
|
||||||
await page.addLocatorHandler(locator, async () => {
|
await page.addLocatorHandler(locator, async () => {
|
||||||
await locator.first().click({ timeout: 10_000, noWaitAfter: true }); // There can be multiple toast pop-ups
|
await locator.first().click(); // There can be multiple toast pop-ups
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// copied over from constants
|
|
||||||
const ON_PREM_RELEASES = new Map([
|
|
||||||
['centos-10', 'CentOS Stream 10'],
|
|
||||||
['fedora-41', 'Fedora Linux 41'],
|
|
||||||
['fedora-42', 'Fedora Linux 42'],
|
|
||||||
['rhel-10', 'Red Hat Enterprise Linux (RHEL) 10'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
export const getHostDistroName = (): string => {
|
|
||||||
const osRelData = readFileSync('/etc/os-release');
|
|
||||||
const lines = osRelData
|
|
||||||
.toString('utf-8')
|
|
||||||
.split('\n')
|
|
||||||
.filter((l) => l !== '');
|
|
||||||
const osRel = {};
|
|
||||||
|
|
||||||
for (const l of lines) {
|
|
||||||
const lineData = l.split('=');
|
|
||||||
(osRel as any)[lineData[0]] = lineData[1].replace(/"/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// strip minor version from rhel
|
|
||||||
const distro = ON_PREM_RELEASES.get(
|
|
||||||
`${(osRel as any)['ID']}-${(osRel as any)['VERSION_ID'].split('.')[0]}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (distro === undefined) {
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
console.error('getHostDistroName failed, os-release config:', osRel);
|
|
||||||
throw new Error('getHostDistroName failed, distro undefined');
|
|
||||||
}
|
|
||||||
|
|
||||||
return distro;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getHostArch = (): string => {
|
|
||||||
return execSync('uname -m').toString('utf-8').replace(/\s/g, '');
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import path from 'path';
|
import { type Page, expect } from '@playwright/test';
|
||||||
|
|
||||||
import { expect, type Page } from '@playwright/test';
|
|
||||||
|
|
||||||
import { closePopupsIfExist, isHosted, togglePreview } from './helpers';
|
import { closePopupsIfExist, isHosted, togglePreview } from './helpers';
|
||||||
import { ibFrame } from './navHelpers';
|
import { ibFrame } from './navHelpers';
|
||||||
|
|
@ -23,38 +21,6 @@ export const login = async (page: Page) => {
|
||||||
return loginCockpit(page, user, password);
|
return loginCockpit(page, user, password);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the user is already authenticated, if not, logs them in
|
|
||||||
* @param page - the page object
|
|
||||||
*/
|
|
||||||
export const ensureAuthenticated = async (page: Page) => {
|
|
||||||
// Navigate to the target page
|
|
||||||
if (isHosted()) {
|
|
||||||
await page.goto('/insights/image-builder/landing');
|
|
||||||
} else {
|
|
||||||
await page.goto('/cockpit-image-builder');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for authentication success indicator
|
|
||||||
const successIndicator = isHosted()
|
|
||||||
? page.getByRole('heading', { name: 'All images' })
|
|
||||||
: ibFrame(page).getByRole('heading', { name: 'All images' });
|
|
||||||
|
|
||||||
let isAuthenticated = false;
|
|
||||||
try {
|
|
||||||
// Give it a 30 second period to load, it's less expensive than having to rerun the test
|
|
||||||
await expect(successIndicator).toBeVisible({ timeout: 30000 });
|
|
||||||
isAuthenticated = true;
|
|
||||||
} catch {
|
|
||||||
isAuthenticated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
// Not authenticated, need to login
|
|
||||||
await login(page);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loginCockpit = async (page: Page, user: string, password: string) => {
|
const loginCockpit = async (page: Page, user: string, password: string) => {
|
||||||
await page.goto('/cockpit-image-builder');
|
await page.goto('/cockpit-image-builder');
|
||||||
|
|
||||||
|
|
@ -65,74 +31,46 @@ const loginCockpit = async (page: Page, user: string, password: string) => {
|
||||||
// image-builder lives inside an iframe
|
// image-builder lives inside an iframe
|
||||||
const frame = ibFrame(page);
|
const frame = ibFrame(page);
|
||||||
|
|
||||||
try {
|
// cockpit-image-builder needs superuser, expect an error message
|
||||||
// Check if the user already has administrative access
|
// when the user does not have admin priviliges
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Administrative access' }),
|
frame.getByRole('heading', { name: 'Access is limited' })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
} catch {
|
await page.getByRole('button', { name: 'Limited access' }).click();
|
||||||
// If not, try to gain it
|
|
||||||
// cockpit-image-builder needs superuser, expect an error message
|
|
||||||
// when the user does not have admin priviliges
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('heading', { name: 'Access is limited' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await page.getByRole('button', { name: 'Limited access' }).click();
|
|
||||||
|
|
||||||
// different popup opens based on type of account (can be passwordless)
|
// different popup opens based on type of account (can be passwordless)
|
||||||
const authenticateButton = page.getByRole('button', {
|
const authenticateButton = page.getByRole('button', { name: 'Authenticate' });
|
||||||
name: 'Authenticate',
|
const closeButton = page.getByText('Close');
|
||||||
});
|
await expect(authenticateButton.or(closeButton)).toBeVisible();
|
||||||
const closeButton = page.getByText('Close');
|
|
||||||
await expect(authenticateButton.or(closeButton)).toBeVisible();
|
|
||||||
|
|
||||||
if (await authenticateButton.isVisible()) {
|
if (await authenticateButton.isVisible()) {
|
||||||
// with password
|
// with password
|
||||||
await page.getByRole('textbox', { name: 'Password' }).fill(password);
|
await page.getByRole('textbox', { name: 'Password' }).fill(password);
|
||||||
await authenticateButton.click();
|
await authenticateButton.click();
|
||||||
}
|
}
|
||||||
if (await closeButton.isVisible()) {
|
if (await closeButton.isVisible()) {
|
||||||
// passwordless
|
// passwordless
|
||||||
await closeButton.click();
|
await closeButton.click();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// expect to have administrative access
|
// expect to have administrative access
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Administrative access' }),
|
page.getByRole('button', { name: 'Administrative access' })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
frame.getByRole('heading', { name: 'All images' }),
|
frame.getByRole('heading', { name: 'All images' })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginConsole = async (page: Page, user: string, password: string) => {
|
const loginConsole = async (page: Page, user: string, password: string) => {
|
||||||
await closePopupsIfExist(page);
|
await closePopupsIfExist(page);
|
||||||
await page.goto('/insights/image-builder/landing');
|
await page.goto('/insights/image-builder/landing');
|
||||||
await page.getByRole('textbox', { name: 'Red Hat login' }).fill(user);
|
await page
|
||||||
|
.getByRole('textbox', { name: 'Red Hat login or email' })
|
||||||
|
.fill(user);
|
||||||
await page.getByRole('button', { name: 'Next' }).click();
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Password' }).fill(password);
|
await page.getByRole('textbox', { name: 'Password' }).fill(password);
|
||||||
await page.getByRole('button', { name: 'Log in' }).click();
|
await page.getByRole('button', { name: 'Log in' }).click();
|
||||||
await togglePreview(page);
|
await togglePreview(page);
|
||||||
await expect(page.getByRole('heading', { name: 'All images' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'All images' })).toBeVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storeStorageStateAndToken = async (page: Page) => {
|
|
||||||
const { cookies } = await page
|
|
||||||
.context()
|
|
||||||
.storageState({ path: path.join(__dirname, '../../.auth/user.json') });
|
|
||||||
if (isHosted()) {
|
|
||||||
// For hosted service, look for cs_jwt token
|
|
||||||
process.env.TOKEN = `Bearer ${
|
|
||||||
cookies.find((cookie) => cookie.name === 'cs_jwt')?.value
|
|
||||||
}`;
|
|
||||||
} else {
|
|
||||||
// For Cockpit, we don't need a TOKEN but we can still store it for consistency
|
|
||||||
const cockpitCookie = cookies.find((cookie) => cookie.name === 'cockpit');
|
|
||||||
if (cockpitCookie) {
|
|
||||||
process.env.TOKEN = cockpitCookie.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,13 @@
|
||||||
import { expect, FrameLocator, Page } from '@playwright/test';
|
import type { FrameLocator, Page } from '@playwright/test';
|
||||||
|
|
||||||
import { getHostArch, getHostDistroName, isHosted } from './helpers';
|
import { isHosted } from './helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the wizard, fills out the "Image Output" step, and navigates to the optional steps
|
* Opens the wizard, fills out the "Image Output" step, and navigates to the optional steps
|
||||||
* @param page - the page object
|
* @param page - the page object
|
||||||
*/
|
*/
|
||||||
export const navigateToOptionalSteps = async (page: Page | FrameLocator) => {
|
export const navigateToOptionalSteps = async (page: Page | FrameLocator) => {
|
||||||
await page.getByRole('button', { name: 'Create image blueprint' }).click();
|
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
||||||
if (!isHosted()) {
|
|
||||||
// wait until the distro and architecture aligns with the host
|
|
||||||
await expect(page.getByTestId('release_select')).toHaveText(
|
|
||||||
getHostDistroName(),
|
|
||||||
);
|
|
||||||
await expect(page.getByTestId('arch_select')).toHaveText(getHostArch());
|
|
||||||
}
|
|
||||||
await page.getByRole('checkbox', { name: 'Virtualization' }).click();
|
await page.getByRole('checkbox', { name: 'Virtualization' }).click();
|
||||||
await page.getByRole('button', { name: 'Next' }).click();
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
};
|
};
|
||||||
|
|
@ -31,15 +24,3 @@ export const ibFrame = (page: Page): FrameLocator | Page => {
|
||||||
.locator('iframe[name="cockpit1\\:localhost\\/cockpit-image-builder"]')
|
.locator('iframe[name="cockpit1\\:localhost\\/cockpit-image-builder"]')
|
||||||
.contentFrame();
|
.contentFrame();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to the landing page of the Image Builder
|
|
||||||
* @param page - the page object
|
|
||||||
*/
|
|
||||||
export const navigateToLandingPage = async (page: Page) => {
|
|
||||||
if (isHosted()) {
|
|
||||||
await page.goto('/insights/image-builder/landing');
|
|
||||||
} else {
|
|
||||||
await page.goto('/cockpit-image-builder');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { expect, FrameLocator, type Page, test } from '@playwright/test';
|
import { expect, FrameLocator, type Page, test } from '@playwright/test';
|
||||||
|
|
||||||
import { closePopupsIfExist, isHosted } from './helpers';
|
import { isHosted } from './helpers';
|
||||||
import { ibFrame, navigateToLandingPage } from './navHelpers';
|
import { ibFrame } from './navHelpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clicks the create button, handles the modal, clicks the button again and selecets the BP in the list
|
* Clicks the create button, handles the modal, clicks the button again and selecets the BP in the list
|
||||||
|
|
@ -10,15 +10,13 @@ import { ibFrame, navigateToLandingPage } from './navHelpers';
|
||||||
*/
|
*/
|
||||||
export const createBlueprint = async (
|
export const createBlueprint = async (
|
||||||
page: Page | FrameLocator,
|
page: Page | FrameLocator,
|
||||||
blueprintName: string,
|
blueprintName: string
|
||||||
) => {
|
) => {
|
||||||
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
||||||
await page.getByRole('button', { name: 'Close' }).first().click();
|
await page.getByRole('button', { name: 'Close' }).first().click();
|
||||||
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Search input' }).fill(blueprintName);
|
await page.getByRole('textbox', { name: 'Search input' }).fill(blueprintName);
|
||||||
// the clickable blueprint cards are a bit awkward, so use the
|
await page.getByTestId('blueprint-card').getByText(blueprintName).click();
|
||||||
// button's id instead
|
|
||||||
await page.locator(`button[id="${blueprintName}"]`).click();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,7 +29,7 @@ export const createBlueprint = async (
|
||||||
*/
|
*/
|
||||||
export const fillInDetails = async (
|
export const fillInDetails = async (
|
||||||
page: Page | FrameLocator,
|
page: Page | FrameLocator,
|
||||||
blueprintName: string,
|
blueprintName: string
|
||||||
) => {
|
) => {
|
||||||
await page.getByRole('listitem').filter({ hasText: 'Details' }).click();
|
await page.getByRole('listitem').filter({ hasText: 'Details' }).click();
|
||||||
await page
|
await page
|
||||||
|
|
@ -67,41 +65,27 @@ export const fillInImageOutputGuest = async (page: Page | FrameLocator) => {
|
||||||
/**
|
/**
|
||||||
* Delete the blueprint with the given name
|
* Delete the blueprint with the given name
|
||||||
* Will locate to the Image Builder page and search for the blueprint first
|
* Will locate to the Image Builder page and search for the blueprint first
|
||||||
* If the blueprint is not found, it will fail gracefully
|
|
||||||
* @param page - the page object
|
* @param page - the page object
|
||||||
* @param blueprintName - the name of the blueprint to delete
|
* @param blueprintName - the name of the blueprint to delete
|
||||||
*/
|
*/
|
||||||
export const deleteBlueprint = async (page: Page, blueprintName: string) => {
|
export const deleteBlueprint = async (page: Page, blueprintName: string) => {
|
||||||
// Since new browser is opened during the BP cleanup, we need to call the popup closer again
|
|
||||||
await closePopupsIfExist(page);
|
|
||||||
await test.step(
|
await test.step(
|
||||||
'Delete the blueprint with name: ' + blueprintName,
|
'Delete the blueprint with name: ' + blueprintName,
|
||||||
async () => {
|
async () => {
|
||||||
// Locate back to the Image Builder page every time because the test can fail at any stage
|
// Locate back to the Image Builder page every time because the test can fail at any stage
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
await frame
|
await frame
|
||||||
.getByRole('textbox', { name: 'Search input' })
|
.getByRole('textbox', { name: 'Search input' })
|
||||||
.fill(blueprintName);
|
.fill(blueprintName);
|
||||||
// Check if no blueprints found -> that means no blueprint was created -> fail gracefully and do not raise error
|
await frame
|
||||||
try {
|
.getByTestId('blueprint-card')
|
||||||
await expect(
|
.getByText(blueprintName)
|
||||||
frame.getByRole('heading', { name: 'No blueprints found' }),
|
.click();
|
||||||
).toBeVisible({ timeout: 5_000 }); // Shorter timeout to avoid hanging uncessarily
|
|
||||||
return; // Fail gracefully, no blueprint to delete
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (error) {
|
|
||||||
// If the No BP heading was not found, it means the blueprint (possibly) was created -> continue with deletion
|
|
||||||
}
|
|
||||||
|
|
||||||
// the clickable blueprint cards are a bit awkward, so use the
|
|
||||||
// button's id instead
|
|
||||||
await frame.locator(`button[id="${blueprintName}"]`).click();
|
|
||||||
await frame.getByRole('button', { name: 'Menu toggle' }).click();
|
await frame.getByRole('button', { name: 'Menu toggle' }).click();
|
||||||
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).click();
|
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).click();
|
||||||
await frame.getByRole('button', { name: 'Delete' }).click();
|
await frame.getByRole('button', { name: 'Delete' }).click();
|
||||||
},
|
},
|
||||||
{ box: true },
|
{ box: true }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -129,7 +113,7 @@ export const exportBlueprint = async (page: Page, blueprintName: string) => {
|
||||||
*/
|
*/
|
||||||
export const importBlueprint = async (
|
export const importBlueprint = async (
|
||||||
page: Page | FrameLocator,
|
page: Page | FrameLocator,
|
||||||
blueprintName: string,
|
blueprintName: string
|
||||||
) => {
|
) => {
|
||||||
if (isHosted()) {
|
if (isHosted()) {
|
||||||
await page.getByRole('button', { name: 'Import' }).click();
|
await page.getByRole('button', { name: 'Import' }).click();
|
||||||
|
|
@ -138,7 +122,7 @@ export const importBlueprint = async (
|
||||||
.locator('input[type=file]')
|
.locator('input[type=file]')
|
||||||
.setInputFiles('../../downloads/' + blueprintName + '.json');
|
.setInputFiles('../../downloads/' + blueprintName + '.json');
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('textbox', { name: 'File upload' }),
|
page.getByRole('textbox', { name: 'File upload' })
|
||||||
).not.toBeEmpty();
|
).not.toBeEmpty();
|
||||||
await page.getByRole('button', { name: 'Review and Finish' }).click();
|
await page.getByRole('button', { name: 'Review and Finish' }).click();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,75 @@
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
|
|
||||||
import TOML from '@ltd/j-toml';
|
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { closePopupsIfExist, isHosted } from './helpers/helpers';
|
import { isHosted } from './helpers/helpers';
|
||||||
import { ensureAuthenticated } from './helpers/login';
|
import { login } from './helpers/login';
|
||||||
import { ibFrame, navigateToLandingPage } from './helpers/navHelpers';
|
import { ibFrame } from './helpers/navHelpers';
|
||||||
|
|
||||||
test.describe.serial('test', () => {
|
test.describe.serial('test', () => {
|
||||||
const blueprintName = uuidv4();
|
const blueprintName = uuidv4();
|
||||||
test('create blueprint', async ({ page }) => {
|
test('create blueprint', async ({ page }) => {
|
||||||
await ensureAuthenticated(page);
|
await login(page);
|
||||||
await closePopupsIfExist(page);
|
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Images About image builder' });
|
await frame.getByRole('heading', { name: 'Images About image builder' });
|
||||||
frame.getByRole('heading', { name: 'Blueprints' });
|
await frame.getByRole('heading', { name: 'Blueprints' });
|
||||||
await frame.getByTestId('blueprints-create-button').click();
|
await frame.getByTestId('blueprints-create-button').click();
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Image output' });
|
await frame.getByRole('heading', { name: 'Image output' });
|
||||||
await frame
|
await frame.getByTestId('checkbox-guest-image').click();
|
||||||
.getByRole('checkbox', { name: /Virtualization guest image/i })
|
|
||||||
.click();
|
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
if (isHosted()) {
|
if (isHosted()) {
|
||||||
frame.getByRole('heading', {
|
await frame.getByRole('heading', {
|
||||||
name: 'Register systems using this image',
|
name: 'Register systems using this image',
|
||||||
});
|
});
|
||||||
await page.getByRole('radio', { name: /Register later/i }).click();
|
await page.getByTestId('register-later-radio').click();
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Compliance' });
|
await frame.getByRole('heading', { name: 'Compliance' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'File system configuration' });
|
await frame.getByRole('heading', { name: 'File system configuration' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
if (isHosted()) {
|
if (isHosted()) {
|
||||||
frame.getByRole('heading', { name: 'Repository snapshot' });
|
await frame.getByRole('heading', { name: 'Repository snapshot' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
frame.getByRole('heading', { name: 'Custom repositories' });
|
await frame.getByRole('heading', { name: 'Custom repositories' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Additional packages' });
|
await frame.getByRole('heading', { name: 'Additional packages' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Users' });
|
await frame.getByRole('heading', { name: 'Users' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Timezone' });
|
await frame.getByRole('heading', { name: 'Timezone' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Locale' });
|
await frame.getByRole('heading', { name: 'Locale' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Hostname' });
|
await frame.getByRole('heading', { name: 'Hostname' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Kernel' });
|
await frame.getByRole('heading', { name: 'Kernel' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Firewall' });
|
await frame.getByRole('heading', { name: 'Firewall' });
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Systemd services' });
|
await 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()) {
|
if (isHosted()) {
|
||||||
frame.getByRole('heading', { name: 'Ansible Automation Platform' });
|
await 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHosted()) {
|
await frame.getByRole('heading', { name: 'Details' });
|
||||||
frame.getByRole('heading', { name: 'First boot configuration' });
|
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Details' });
|
|
||||||
await frame.getByTestId('blueprint').fill(blueprintName);
|
await frame.getByTestId('blueprint').fill(blueprintName);
|
||||||
await expect(frame.getByTestId('blueprint')).toHaveValue(blueprintName);
|
await expect(frame.getByTestId('blueprint')).toHaveValue(blueprintName);
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
@ -92,32 +79,22 @@ test.describe.serial('test', () => {
|
||||||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
frame.locator('.pf-v6-c-card__title-text').getByText(
|
frame.locator('.pf-v5-c-card__title-text').getByText(blueprintName)
|
||||||
// if the name is too long, the blueprint card will have a truncated name.
|
|
||||||
blueprintName.length > 24
|
|
||||||
? blueprintName.slice(0, 24) + '...'
|
|
||||||
: blueprintName,
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('edit blueprint', async ({ page }) => {
|
test('edit blueprint', async ({ page }) => {
|
||||||
await ensureAuthenticated(page);
|
|
||||||
await closePopupsIfExist(page);
|
|
||||||
// package searching is really slow the first time in cockpit
|
// package searching is really slow the first time in cockpit
|
||||||
if (!isHosted()) {
|
if (!isHosted()) {
|
||||||
test.setTimeout(300000);
|
test.setTimeout(300000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to IB landing page and get the frame
|
await login(page);
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
await frame
|
await frame
|
||||||
.getByRole('textbox', { name: 'Search input' })
|
.getByRole('textbox', { name: 'Search input' })
|
||||||
.fill(blueprintName);
|
.fill(blueprintName);
|
||||||
// the clickable blueprint cards are a bit awkward, so use the
|
await frame.getByText(blueprintName, { exact: true }).first().click();
|
||||||
// button's id instead
|
|
||||||
await frame.locator(`button[id="${blueprintName}"]`).click();
|
|
||||||
|
|
||||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
||||||
await frame.getByRole('button', { name: 'Additional packages' }).click();
|
await frame.getByRole('button', { name: 'Additional packages' }).click();
|
||||||
|
|
@ -125,37 +102,30 @@ test.describe.serial('test', () => {
|
||||||
.getByTestId('packages-search-input')
|
.getByTestId('packages-search-input')
|
||||||
.locator('input')
|
.locator('input')
|
||||||
.fill('osbuild-composer');
|
.fill('osbuild-composer');
|
||||||
frame.getByTestId('packages-table').getByText('Searching');
|
await frame.getByTestId('packages-table').getByText('Searching');
|
||||||
frame.getByRole('gridcell', { name: 'osbuild-composer' }).first();
|
await frame.getByRole('gridcell', { name: 'osbuild-composer' }).first();
|
||||||
await frame.getByRole('checkbox', { name: 'Select row 0' }).check();
|
await frame.getByRole('checkbox', { name: 'Select row 0' }).check();
|
||||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||||
await frame.getByRole('button', { name: 'About packages' }).click();
|
await frame.getByRole('button', { name: 'About packages' }).click();
|
||||||
frame.getByRole('gridcell', { name: 'osbuild-composer' });
|
await frame.getByRole('gridcell', { name: 'osbuild-composer' });
|
||||||
await frame.getByRole('button', { name: 'Close', exact: true }).click();
|
|
||||||
await frame
|
await frame
|
||||||
.getByRole('button', { name: 'Save changes to blueprint' })
|
.getByRole('button', { name: 'Save changes to blueprint' })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
||||||
await frame.getByRole('button', { name: 'About packages' }).click();
|
await frame.getByRole('button', { name: 'About packages' }).click();
|
||||||
frame.getByRole('gridcell', { name: 'osbuild-composer' });
|
await frame.getByRole('gridcell', { name: 'osbuild-composer' });
|
||||||
await frame.getByRole('button', { name: 'Close', exact: true }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Cancel', exact: true }).click();
|
await frame.getByRole('button', { name: 'Cancel', exact: true }).click();
|
||||||
frame.getByRole('heading', { name: 'All images' });
|
await frame.getByRole('heading', { name: 'All images' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('build blueprint', async ({ page }) => {
|
test('build blueprint', async ({ page }) => {
|
||||||
await ensureAuthenticated(page);
|
await login(page);
|
||||||
await closePopupsIfExist(page);
|
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
await frame
|
await frame
|
||||||
.getByRole('textbox', { name: 'Search input' })
|
.getByRole('textbox', { name: 'Search input' })
|
||||||
.fill(blueprintName);
|
.fill(blueprintName);
|
||||||
// the clickable blueprint cards are a bit awkward, so use the
|
await frame.getByText(blueprintName, { exact: true }).first().click();
|
||||||
// button's id instead
|
|
||||||
await frame.locator(`button[id="${blueprintName}"]`).click();
|
|
||||||
await frame.getByTestId('blueprint-build-image-menu-option').click();
|
await frame.getByTestId('blueprint-build-image-menu-option').click();
|
||||||
|
|
||||||
// make sure the image is present
|
// make sure the image is present
|
||||||
|
|
@ -163,152 +133,18 @@ test.describe.serial('test', () => {
|
||||||
.getByTestId('images-table')
|
.getByTestId('images-table')
|
||||||
.getByRole('button', { name: 'Details' })
|
.getByRole('button', { name: 'Details' })
|
||||||
.click();
|
.click();
|
||||||
frame.getByText('Build Information');
|
await frame.getByText('Build Information');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete blueprint', async ({ page }) => {
|
test('delete blueprint', async ({ page }) => {
|
||||||
await ensureAuthenticated(page);
|
await login(page);
|
||||||
await closePopupsIfExist(page);
|
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
const frame = await ibFrame(page);
|
const frame = await ibFrame(page);
|
||||||
await frame
|
await frame
|
||||||
.getByRole('textbox', { name: 'Search input' })
|
.getByRole('textbox', { name: 'Search input' })
|
||||||
.fill(blueprintName);
|
.fill(blueprintName);
|
||||||
// the clickable blueprint cards are a bit awkward, so use the
|
await frame.getByText(blueprintName, { exact: true }).first().click();
|
||||||
// button's id instead
|
await frame.getByTestId('blueprint-action-menu-toggle').click();
|
||||||
await frame.locator(`button[id="${blueprintName}"]`).click();
|
|
||||||
await frame.getByRole('button', { name: /blueprint menu toggle/i }).click();
|
|
||||||
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).click();
|
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).click();
|
||||||
await frame.getByRole('button', { name: 'Delete' }).click();
|
await frame.getByRole('button', { name: 'Delete' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cockpit worker config', async ({ page }) => {
|
|
||||||
if (isHosted()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureAuthenticated(page);
|
|
||||||
await closePopupsIfExist(page);
|
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
await page.goto('/cockpit-image-builder');
|
|
||||||
const frame = ibFrame(page);
|
|
||||||
|
|
||||||
const header = frame.getByText('Configure AWS Uploads');
|
|
||||||
if (!(await header.isVisible())) {
|
|
||||||
await frame
|
|
||||||
.getByRole('button', { name: 'Configure Cloud Providers' })
|
|
||||||
.click();
|
|
||||||
await expect(header).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
const bucket = 'cockpit-ib-playwright-bucket';
|
|
||||||
const credentials = '/test/credentials';
|
|
||||||
const switchInput = frame.locator('#aws-config-switch');
|
|
||||||
await expect(switchInput).toBeVisible();
|
|
||||||
|
|
||||||
// introduce a wait time, since it takes some time to load the
|
|
||||||
// worker config file.
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// If this test fails for any reason, the config should already be loaded
|
|
||||||
// and visible on the retury. If it is go back to the landing page
|
|
||||||
if (await switchInput.isChecked()) {
|
|
||||||
await frame.getByRole('button', { name: 'Cancel' }).click();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('heading', { name: 'All images' }),
|
|
||||||
).toBeVisible();
|
|
||||||
} else {
|
|
||||||
const switchToggle = frame.locator('.pf-v6-c-switch');
|
|
||||||
await switchToggle.click();
|
|
||||||
|
|
||||||
await frame
|
|
||||||
.getByPlaceholder('AWS bucket')
|
|
||||||
// this doesn't need to exist, we're just testing that
|
|
||||||
// the form works as expected
|
|
||||||
.fill(bucket);
|
|
||||||
await frame.getByPlaceholder('Path to AWS credentials').fill(credentials);
|
|
||||||
await frame.getByRole('button', { name: 'Submit' }).click();
|
|
||||||
await expect(
|
|
||||||
frame.getByRole('heading', { name: 'All images' }),
|
|
||||||
).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
await frame
|
|
||||||
.getByRole('button', { name: 'Configure Cloud Providers' })
|
|
||||||
.click();
|
|
||||||
await expect(header).toBeVisible();
|
|
||||||
|
|
||||||
// introduce a wait time, since it takes some time to load the
|
|
||||||
// worker config file.
|
|
||||||
await page.waitForTimeout(1500);
|
|
||||||
|
|
||||||
await expect(frame.locator('#aws-config-switch')).toBeChecked();
|
|
||||||
|
|
||||||
await expect(frame.getByPlaceholder('AWS bucket')).toHaveValue(bucket);
|
|
||||||
await expect(frame.getByPlaceholder('Path to AWS credentials')).toHaveValue(
|
|
||||||
credentials,
|
|
||||||
);
|
|
||||||
await frame.getByRole('button', { name: 'Cancel' }).click();
|
|
||||||
|
|
||||||
const config = readFileSync('/etc/osbuild-worker/osbuild-worker.toml');
|
|
||||||
// this is for testing, the field `aws` should exist
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const parsed = TOML.parse(config) as any;
|
|
||||||
expect(parsed.aws?.bucket).toBe(bucket);
|
|
||||||
expect(parsed.aws?.credentials).toBe(credentials);
|
|
||||||
});
|
|
||||||
|
|
||||||
const cockpitBlueprintname = uuidv4();
|
|
||||||
test('cockpit cloud upload', async ({ page }) => {
|
|
||||||
if (isHosted()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureAuthenticated(page);
|
|
||||||
await closePopupsIfExist(page);
|
|
||||||
// Navigate to IB landing page and get the frame
|
|
||||||
await navigateToLandingPage(page);
|
|
||||||
await page.goto('/cockpit-image-builder');
|
|
||||||
const frame = ibFrame(page);
|
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Images About image builder' });
|
|
||||||
frame.getByRole('heading', { name: 'Blueprints' });
|
|
||||||
await frame.getByTestId('blueprints-create-button').click();
|
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Image output' });
|
|
||||||
// the first card should be the AWS card
|
|
||||||
await frame.locator('.pf-v6-c-card').first().click();
|
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
|
||||||
await frame.getByRole('button', { name: 'Back', exact: true }).click();
|
|
||||||
|
|
||||||
frame.getByRole('heading', { name: 'Details' });
|
|
||||||
await frame.getByTestId('blueprint').fill(cockpitBlueprintname);
|
|
||||||
await expect(frame.getByTestId('blueprint')).toHaveValue(
|
|
||||||
cockpitBlueprintname,
|
|
||||||
);
|
|
||||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
|
||||||
|
|
||||||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
|
||||||
await frame.getByTestId('close-button-saveandbuild-modal').click();
|
|
||||||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
|
||||||
|
|
||||||
await frame
|
|
||||||
.getByRole('textbox', { name: 'Search input' })
|
|
||||||
.fill(cockpitBlueprintname);
|
|
||||||
// the clickable blueprint cards are a bit awkward, so use the
|
|
||||||
// button's id instead
|
|
||||||
await frame.locator(`button[id="${cockpitBlueprintname}"]`).click();
|
|
||||||
await frame.getByTestId('blueprint-build-image-menu-option').click();
|
|
||||||
|
|
||||||
// make sure the image is present
|
|
||||||
await frame
|
|
||||||
.getByTestId('images-table')
|
|
||||||
.getByRole('button', { name: 'Details' })
|
|
||||||
.click();
|
|
||||||
frame.getByText('Build Information');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
57
pr_check.sh
Executable file
57
pr_check.sh
Executable file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# --------------------------------------------
|
||||||
|
# Export vars for helper scripts to use
|
||||||
|
# --------------------------------------------
|
||||||
|
# name of app-sre "application" folder this component lives in; needs to match for quay
|
||||||
|
export COMPONENT_NAME="image-builder-frontend"
|
||||||
|
# IMAGE should match the quay repo set by app.yaml in app-interface
|
||||||
|
export IMAGE="quay.io/cloudservices/image-builder-frontend"
|
||||||
|
export WORKSPACE=${WORKSPACE:-$APP_ROOT} # if running in jenkins, use the build's workspace
|
||||||
|
export APP_ROOT=$(pwd)
|
||||||
|
#16 is the default Node version. Change this to override it.
|
||||||
|
export NODE_BUILD_VERSION=20
|
||||||
|
# skip unit tests on frontend-build
|
||||||
|
export SKIP_VERIFY=True
|
||||||
|
COMMON_BUILDER=https://raw.githubusercontent.com/RedHatInsights/insights-frontend-builder-common/master
|
||||||
|
|
||||||
|
# --------------------------------------------
|
||||||
|
# Options that must be configured by app owner
|
||||||
|
# --------------------------------------------
|
||||||
|
export IQE_PLUGINS="image-builder"
|
||||||
|
export IQE_CJI_TIMEOUT="90m"
|
||||||
|
export IQE_MARKER_EXPRESSION="fe_pr_check"
|
||||||
|
export IQE_SELENIUM="true"
|
||||||
|
export IQE_ENV="ephemeral"
|
||||||
|
export IQE_IMAGE_TAG="image-builder"
|
||||||
|
export IQE_PARALLEL_ENABLED="false"
|
||||||
|
export RESERVE_DURATION="2h"
|
||||||
|
|
||||||
|
# bootstrap bonfire and it's config
|
||||||
|
CICD_URL=https://raw.githubusercontent.com/RedHatInsights/bonfire/master/cicd
|
||||||
|
curl -s "$CICD_URL"/bootstrap.sh >.cicd_bootstrap.sh && source .cicd_bootstrap.sh
|
||||||
|
|
||||||
|
# # source is preferred to | bash -s in this case to avoid a subshell
|
||||||
|
source <(curl -sSL $COMMON_BUILDER/src/frontend-build.sh)
|
||||||
|
|
||||||
|
# reserve ephemeral namespace
|
||||||
|
export DEPLOY_FRONTENDS="true"
|
||||||
|
export EXTRA_DEPLOY_ARGS="provisioning sources rhsm-api-proxy --set-template-ref rhsm-api-proxy=master"
|
||||||
|
export APP_NAME="image-builder-crc"
|
||||||
|
export DEPLOY_TIMEOUT="1200"
|
||||||
|
export REF_ENV="insights-stage"
|
||||||
|
# overwrites any resource limits imposed by bonfire
|
||||||
|
export COMPONENTS_W_RESOURCES="compliance notifications-backend notifications-engine"
|
||||||
|
|
||||||
|
source "$CICD_ROOT"/deploy_ephemeral_env.sh
|
||||||
|
|
||||||
|
# Run smoke tests using a ClowdJobInvocation (preferred)
|
||||||
|
# The contents of this script can be found at:
|
||||||
|
# https://raw.githubusercontent.com/RedHatInsights/bonfire/master/cicd/cji_smoke_test.sh
|
||||||
|
export COMPONENT_NAME="image-builder"
|
||||||
|
source "$CICD_ROOT"/cji_smoke_test.sh
|
||||||
|
|
||||||
|
# Post a comment with test run IDs to the PR
|
||||||
|
# The contents of this script can be found at:
|
||||||
|
# https://raw.githubusercontent.com/RedHatInsights/bonfire/master/cicd/post_test_results.sh
|
||||||
|
source "$CICD_ROOT"/post_test_results.sh
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
summary: run playwright tests
|
|
||||||
test: ./playwright_tests.sh
|
|
||||||
require:
|
|
||||||
- cockpit-image-builder
|
|
||||||
- podman
|
|
||||||
- nodejs
|
|
||||||
- nodejs-npm
|
|
||||||
duration: 30m
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
TMT_SOURCE_DIR=${TMT_SOURCE_DIR:-}
|
# As playwright isn't supported on fedora/el, install dependencies
|
||||||
if [ -n "$TMT_SOURCE_DIR" ]; then
|
# beforehand.
|
||||||
# Move to the directory with sources
|
sudo dnf install -y \
|
||||||
cd "${TMT_SOURCE_DIR}/cockpit-image-builder"
|
alsa-lib \
|
||||||
npm ci
|
libXrandr-devel \
|
||||||
elif [ "${CI:-}" != "true" ]; then
|
libXdamage-devel \
|
||||||
# packit drops us into the schutzbot directory
|
libXcomposite-devel \
|
||||||
cd ../
|
at-spi2-atk-devel \
|
||||||
npm ci
|
cups \
|
||||||
fi
|
atk
|
||||||
|
|
||||||
sudo systemctl enable --now cockpit.socket
|
sudo systemctl enable --now cockpit.socket
|
||||||
|
|
||||||
|
|
@ -19,13 +19,10 @@ sudo usermod -aG wheel admin
|
||||||
echo "admin ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/admin-nopasswd"
|
echo "admin ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/admin-nopasswd"
|
||||||
|
|
||||||
function upload_artifacts {
|
function upload_artifacts {
|
||||||
if [ -n "${TMT_TEST_DATA:-}" ]; then
|
mkdir -p /tmp/artifacts/extra-screenshots
|
||||||
mv playwright-report "$TMT_TEST_DATA"/playwright-report
|
USER="$(whoami)"
|
||||||
else
|
sudo chown -R "$USER:$USER" playwright-report
|
||||||
USER="$(whoami)"
|
mv playwright-report /tmp/artifacts/
|
||||||
sudo chown -R "$USER:$USER" playwright-report
|
|
||||||
mv playwright-report /tmp/artifacts/
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
trap upload_artifacts EXIT
|
trap upload_artifacts EXIT
|
||||||
|
|
||||||
|
|
@ -76,12 +73,10 @@ sudo podman run \
|
||||||
-e "CI=true" \
|
-e "CI=true" \
|
||||||
-e "PLAYWRIGHT_USER=admin" \
|
-e "PLAYWRIGHT_USER=admin" \
|
||||||
-e "PLAYWRIGHT_PASSWORD=foobar" \
|
-e "PLAYWRIGHT_PASSWORD=foobar" \
|
||||||
-e "CURRENTS_PROJECT_ID=${CURRENTS_PROJECT_ID:-}" \
|
-e "CURRENTS_PROJECT_ID=$CURRENTS_PROJECT_ID" \
|
||||||
-e "CURRENTS_RECORD_KEY=${CURRENTS_RECORD_KEY:-}" \
|
-e "CURRENTS_RECORD_KEY=$CURRENTS_RECORD_KEY" \
|
||||||
--net=host \
|
--net=host \
|
||||||
-v "$PWD:/tests" \
|
-v "$PWD:/tests" \
|
||||||
-v '/etc:/etc' \
|
|
||||||
-v '/etc/os-release:/etc/os-release' \
|
|
||||||
--privileged \
|
--privileged \
|
||||||
--rm \
|
--rm \
|
||||||
--init \
|
--init \
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
cf0a810fd3b75fa27139746c4dfe72222e13dcba
|
7b4735d287dd0950e0a6f47dde65b62b0f239da1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# if a user is logged in to the runner, wait until they're done
|
# if a user is logged in to the runner, wait until they're done
|
||||||
while (( $(who -u | grep -v '?' | wc -l) > 0 )); do
|
while (( $(who -s | wc -l) > 0 )); do
|
||||||
echo "Waiting for user(s) to log off"
|
echo "Waiting for user(s) to log off"
|
||||||
sleep 30
|
sleep 30
|
||||||
done
|
done
|
||||||
|
|
|
||||||
19
src/App.tsx
19
src/App.tsx
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import NotificationsProvider from '@redhat-cloud-services/frontend-components-notifications/NotificationsProvider';
|
import NotificationsPortal from '@redhat-cloud-services/frontend-components-notifications/NotificationPortal';
|
||||||
import '@patternfly/patternfly/patternfly-addons.css';
|
import '@patternfly/patternfly/patternfly-addons.css';
|
||||||
|
|
||||||
import { Router } from './Router';
|
import { Router } from './Router';
|
||||||
|
|
@ -14,21 +14,10 @@ const App = () => {
|
||||||
hideGlobalFilter(true);
|
hideGlobalFilter(true);
|
||||||
}, [hideGlobalFilter, updateDocumentTitle]);
|
}, [hideGlobalFilter, updateDocumentTitle]);
|
||||||
|
|
||||||
// Necessary for in-page wizard overflow to behave properly
|
|
||||||
// The .chr-render class is defined in Insights Chrome:
|
|
||||||
// https://github.com/RedHatInsights/insights-chrome/blob/fe573705020ff64003ac9e6101aa978b471fe6f2/src/sass/chrome.scss#L82
|
|
||||||
useEffect(() => {
|
|
||||||
const chrRenderDiv = document.querySelector('.chr-render');
|
|
||||||
if (chrRenderDiv) {
|
|
||||||
(chrRenderDiv as HTMLElement).style.overflow = 'auto';
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<NotificationsProvider>
|
<NotificationsPortal />
|
||||||
<Router />
|
<Router />
|
||||||
</NotificationsProvider>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
@font-face {
|
|
||||||
font-family: 'Red Hat Text';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400 500;
|
|
||||||
src: url('/cockpit/static/fonts/RedHatText/RedHatTextVF.woff2')
|
|
||||||
format('woff2-variations');
|
|
||||||
font-display: fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Red Hat Text';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400 500;
|
|
||||||
src: url('/cockpit/static/fonts/RedHatText/RedHatTextVF-Italic.woff2')
|
|
||||||
format('woff2-variations');
|
|
||||||
font-display: fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Red Hat Display';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400 700;
|
|
||||||
src: url('/cockpit/static/fonts/RedHatDisplay/RedHatDisplayVF.woff2')
|
|
||||||
format('woff2-variations');
|
|
||||||
font-display: fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Red Hat Display';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400 700;
|
|
||||||
src: url('/cockpit/static/fonts/RedHatDisplay/RedHatDisplayVF-Italic.woff2')
|
|
||||||
format('woff2-variations');
|
|
||||||
font-display: fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: RedHatText;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url('/cockpit/static/fonts/RedHatText-Regular.woff2') format('woff2');
|
|
||||||
font-display: fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: RedHatText;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url('/cockpit/static/fonts/RedHatText-Medium.woff2') format('woff2');
|
|
||||||
font-display: fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override as PF Page doesn't allow empty masthead and sidebar
|
|
||||||
@media (min-width: 75rem) {
|
|
||||||
.pf-v6-c-page.no-masthead-sidebar {
|
|
||||||
/* custom class to scope this style to a specific page component instance */
|
|
||||||
--pf-v6-c-page__main-container--GridArea: var(
|
|
||||||
--pf-v6-c-page--masthead--main-container--GridArea
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pf-v6-c-page__main-section {
|
|
||||||
padding-inline: 0;
|
|
||||||
padding-block-start: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pf-v6-c-page__main > section.pf-v6-c-page__main-section:not(.pf-m-padding) {
|
|
||||||
padding-inline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pf-v6-c-card {
|
|
||||||
&.pf-m-clickable::before,
|
|
||||||
&.pf-m-selectable::before {
|
|
||||||
border: var(--pf-v6-c-card--BorderColor) var(--pf-v6-c-card--BorderStyle)
|
|
||||||
var(--pf-v6-c-card--BorderWidth) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,14 +3,11 @@ import '@patternfly/patternfly/patternfly-addons.css';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import 'cockpit-dark-theme';
|
import NotificationsPortal from '@redhat-cloud-services/frontend-components-notifications/NotificationPortal';
|
||||||
import { Page, PageSection } from '@patternfly/react-core';
|
|
||||||
import NotificationsProvider from '@redhat-cloud-services/frontend-components-notifications/NotificationsProvider';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import './AppCockpit.scss';
|
|
||||||
import { NotReady, RequireAdmin } from './Components/Cockpit';
|
import { NotReady, RequireAdmin } from './Components/Cockpit';
|
||||||
import { Router } from './Router';
|
import { Router } from './Router';
|
||||||
import { onPremStore as store } from './store';
|
import { onPremStore as store } from './store';
|
||||||
|
|
@ -31,21 +28,16 @@ const Application = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<NotificationsProvider>
|
<NotificationsPortal />
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Router />
|
<Router />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</NotificationsProvider>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const ImageBuilder = () => (
|
const ImageBuilder = () => (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Page className='no-masthead-sidebar' isContentFilled>
|
<Application />
|
||||||
<PageSection>
|
|
||||||
<Application />
|
|
||||||
</PageSection>
|
|
||||||
</Page>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export const BlueprintActionsMenu: React.FunctionComponent<
|
||||||
setShowBlueprintActionsMenu(!showBlueprintActionsMenu);
|
setShowBlueprintActionsMenu(!showBlueprintActionsMenu);
|
||||||
};
|
};
|
||||||
const importExportFlag = useFlagWithEphemDefault(
|
const importExportFlag = useFlagWithEphemDefault(
|
||||||
'image-builder.import.enabled',
|
'image-builder.import.enabled'
|
||||||
);
|
);
|
||||||
|
|
||||||
const [trigger] = useLazyExportBlueprintQuery();
|
const [trigger] = useLazyExportBlueprintQuery();
|
||||||
|
|
@ -58,10 +58,11 @@ export const BlueprintActionsMenu: React.FunctionComponent<
|
||||||
ref={toggleRef}
|
ref={toggleRef}
|
||||||
isExpanded={showBlueprintActionsMenu}
|
isExpanded={showBlueprintActionsMenu}
|
||||||
onClick={() => setShowBlueprintActionsMenu(!showBlueprintActionsMenu)}
|
onClick={() => setShowBlueprintActionsMenu(!showBlueprintActionsMenu)}
|
||||||
variant='plain'
|
variant="plain"
|
||||||
aria-label='blueprint menu toggle'
|
aria-label="blueprint menu toggle"
|
||||||
|
data-testid="blueprint-action-menu-toggle"
|
||||||
>
|
>
|
||||||
<EllipsisVIcon aria-hidden='true' />
|
<EllipsisVIcon aria-hidden="true" />
|
||||||
</MenuToggle>
|
</MenuToggle>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -81,7 +82,7 @@ export const BlueprintActionsMenu: React.FunctionComponent<
|
||||||
|
|
||||||
async function handleExportBlueprint(
|
async function handleExportBlueprint(
|
||||||
blueprintName: string,
|
blueprintName: string,
|
||||||
blueprint: BlueprintExportResponse,
|
blueprint: BlueprintExportResponse
|
||||||
) {
|
) {
|
||||||
const jsonData = JSON.stringify(blueprint, null, 2);
|
const jsonData = JSON.stringify(blueprint, null, 2);
|
||||||
const blob = new Blob([jsonData], { type: 'application/json' });
|
const blob = new Blob([jsonData], { type: 'application/json' });
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,22 @@ import React from 'react';
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
|
CardBody,
|
||||||
|
CardFooter,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
|
|
||||||
import {
|
import {
|
||||||
selectSelectedBlueprintId,
|
selectSelectedBlueprintId,
|
||||||
setBlueprintId,
|
setBlueprintId,
|
||||||
} from '../../store/BlueprintSlice';
|
} from '../../store/BlueprintSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||||
import { BlueprintItem } from '../../store/imageBuilderApi';
|
import {
|
||||||
|
BlueprintItem,
|
||||||
|
useDeleteBlueprintMutation,
|
||||||
|
} from '../../store/imageBuilderApi';
|
||||||
|
|
||||||
type blueprintProps = {
|
type blueprintProps = {
|
||||||
blueprint: BlueprintItem;
|
blueprint: BlueprintItem;
|
||||||
|
|
@ -26,45 +28,28 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
|
||||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { isLoading } = useDeleteBlueprintMutation({
|
const [, { isLoading }] = useDeleteBlueprintMutation({
|
||||||
fixedCacheKey: 'delete-blueprint',
|
fixedCacheKey: 'delete-blueprint',
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card
|
<Card
|
||||||
isClicked={blueprint.id === selectedBlueprintId}
|
isSelected={blueprint.id === selectedBlueprintId}
|
||||||
data-testid={`blueprint-card`}
|
data-testid={`blueprint-card`}
|
||||||
isCompact
|
isCompact
|
||||||
isClickable
|
isClickable
|
||||||
onClick={() => dispatch(setBlueprintId(blueprint.id))}
|
onClick={() => dispatch(setBlueprintId(blueprint.id))}
|
||||||
|
isSelectableRaised
|
||||||
|
hasSelectableInput
|
||||||
|
selectableInputAriaLabel={`Select blueprint ${blueprint.name}`}
|
||||||
>
|
>
|
||||||
<CardHeader
|
<CardHeader data-testid={blueprint.id}>
|
||||||
data-testid={blueprint.id}
|
<CardTitle>
|
||||||
selectableActions={{
|
|
||||||
name: blueprint.name,
|
|
||||||
// use the name rather than the id. This helps us
|
|
||||||
// chose the correct item in the playwright tests
|
|
||||||
selectableActionId: blueprint.name,
|
|
||||||
selectableActionAriaLabel: blueprint.name,
|
|
||||||
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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,14 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DiffEditor } from '@monaco-editor/react';
|
import { DiffEditor } from '@monaco-editor/react';
|
||||||
import {
|
import { Button, Modal, ModalVariant } from '@patternfly/react-core';
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalVariant,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { BuildImagesButton } from './BuildImagesButton';
|
import { BuildImagesButton } from './BuildImagesButton';
|
||||||
|
|
||||||
|
|
@ -34,11 +27,11 @@ const BlueprintDiffModal = ({
|
||||||
|
|
||||||
const { data: baseBlueprint } = useGetBlueprintQuery(
|
const { data: baseBlueprint } = useGetBlueprintQuery(
|
||||||
{ id: selectedBlueprintId as string, version: baseVersion || -1 },
|
{ id: selectedBlueprintId as string, version: baseVersion || -1 },
|
||||||
{ skip: !selectedBlueprintId || !baseVersion },
|
{ skip: !selectedBlueprintId || !baseVersion }
|
||||||
);
|
);
|
||||||
const { data: blueprint } = useGetBlueprintQuery(
|
const { data: blueprint } = useGetBlueprintQuery(
|
||||||
{ id: selectedBlueprintId as string },
|
{ id: selectedBlueprintId as string },
|
||||||
{ skip: !selectedBlueprintId },
|
{ skip: !selectedBlueprintId }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!baseBlueprint || !blueprint) {
|
if (!baseBlueprint || !blueprint) {
|
||||||
|
|
@ -46,32 +39,32 @@ const BlueprintDiffModal = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal variant={ModalVariant.large} isOpen={isOpen} onClose={onClose}>
|
<Modal
|
||||||
<ModalHeader
|
variant={ModalVariant.large}
|
||||||
title={`Compare ${blueprintName || ''} versions`}
|
titleIconVariant={'info'}
|
||||||
titleIconVariant={'info'}
|
isOpen={isOpen}
|
||||||
/>
|
onClose={onClose}
|
||||||
<ModalBody>
|
title={`Compare ${blueprintName || ''} versions`}
|
||||||
<DiffEditor
|
actions={[
|
||||||
height='90vh'
|
<BuildImagesButton key="build-button">
|
||||||
language='json'
|
|
||||||
original={JSON.stringify(baseBlueprint, undefined, 2)}
|
|
||||||
modified={JSON.stringify(blueprint, undefined, 2)}
|
|
||||||
/>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<BuildImagesButton key='build-button'>
|
|
||||||
Synchronize images
|
Synchronize images
|
||||||
</BuildImagesButton>
|
</BuildImagesButton>,
|
||||||
<Button
|
<Button
|
||||||
key='cancel-button'
|
key="cancel-button"
|
||||||
variant='link'
|
variant="link"
|
||||||
type='button'
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>,
|
||||||
</ModalFooter>
|
]}
|
||||||
|
>
|
||||||
|
<DiffEditor
|
||||||
|
height="90vh"
|
||||||
|
language="json"
|
||||||
|
original={JSON.stringify(baseBlueprint, undefined, 2)}
|
||||||
|
modified={JSON.stringify(blueprint, undefined, 2)}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/Me
|
||||||
import { FilterIcon } from '@patternfly/react-icons';
|
import { FilterIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
versionFilterType,
|
||||||
selectBlueprintVersionFilter,
|
selectBlueprintVersionFilter,
|
||||||
setBlueprintVersionFilter,
|
setBlueprintVersionFilter,
|
||||||
versionFilterType,
|
|
||||||
} from '../../store/BlueprintSlice';
|
} from '../../store/BlueprintSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ const BlueprintVersionFilter: React.FC<blueprintVersionFilterProps> = ({
|
||||||
|
|
||||||
const onSelect = (
|
const onSelect = (
|
||||||
_event: React.MouseEvent<Element, MouseEvent> | undefined,
|
_event: React.MouseEvent<Element, MouseEvent> | undefined,
|
||||||
value: versionFilterType,
|
value: versionFilterType
|
||||||
) => {
|
) => {
|
||||||
dispatch(setBlueprintVersionFilter(value));
|
dispatch(setBlueprintVersionFilter(value));
|
||||||
if (onFilterChange) onFilterChange();
|
if (onFilterChange) onFilterChange();
|
||||||
|
|
@ -58,10 +58,10 @@ const BlueprintVersionFilter: React.FC<blueprintVersionFilterProps> = ({
|
||||||
shouldFocusToggleOnSelect
|
shouldFocusToggleOnSelect
|
||||||
>
|
>
|
||||||
<DropdownList>
|
<DropdownList>
|
||||||
<DropdownItem value={'all'} key='all'>
|
<DropdownItem value={'all'} key="all">
|
||||||
All versions
|
All versions
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem value={'latest'} key='newest'>
|
<DropdownItem value={'latest'} key="newest">
|
||||||
Newest
|
Newest
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownList>
|
</DropdownList>
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ const BlueprintsPagination = () => {
|
||||||
page={currPage}
|
page={currPage}
|
||||||
onSetPage={onSetPage}
|
onSetPage={onSetPage}
|
||||||
onPerPageSelect={onPerPageSelect}
|
onPerPageSelect={onPerPageSelect}
|
||||||
widgetId='blueprints-pagination-bottom'
|
widgetId="blueprints-pagination-bottom"
|
||||||
data-testid='blueprints-pagination-bottom'
|
data-testid="blueprints-pagination-bottom"
|
||||||
isCompact
|
isCompact
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Bullseye,
|
Bullseye,
|
||||||
|
|
@ -7,6 +7,8 @@ import {
|
||||||
EmptyStateActions,
|
EmptyStateActions,
|
||||||
EmptyStateBody,
|
EmptyStateBody,
|
||||||
EmptyStateFooter,
|
EmptyStateFooter,
|
||||||
|
EmptyStateHeader,
|
||||||
|
EmptyStateIcon,
|
||||||
Flex,
|
Flex,
|
||||||
FlexItem,
|
FlexItem,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
|
|
@ -17,6 +19,7 @@ import {
|
||||||
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
||||||
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
@ -25,10 +28,9 @@ import BlueprintsPagination from './BlueprintsPagination';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEBOUNCED_SEARCH_WAIT_TIME,
|
DEBOUNCED_SEARCH_WAIT_TIME,
|
||||||
PAGINATION_LIMIT,
|
|
||||||
PAGINATION_OFFSET,
|
PAGINATION_OFFSET,
|
||||||
|
PAGINATION_LIMIT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useGetUser } from '../../Hooks';
|
|
||||||
import { useGetBlueprintsQuery } from '../../store/backendApi';
|
import { useGetBlueprintsQuery } from '../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
selectBlueprintSearchInput,
|
selectBlueprintSearchInput,
|
||||||
|
|
@ -46,6 +48,7 @@ import {
|
||||||
} from '../../store/imageBuilderApi';
|
} from '../../store/imageBuilderApi';
|
||||||
import { imageBuilderApi } from '../../store/service/enhancedImageBuilderApi';
|
import { imageBuilderApi } from '../../store/service/enhancedImageBuilderApi';
|
||||||
import { resolveRelPath } from '../../Utilities/path';
|
import { resolveRelPath } from '../../Utilities/path';
|
||||||
|
import { useGetEnvironment } from '../../Utilities/useGetEnvironment';
|
||||||
|
|
||||||
type blueprintSearchProps = {
|
type blueprintSearchProps = {
|
||||||
blueprintsTotal: number;
|
blueprintsTotal: number;
|
||||||
|
|
@ -60,8 +63,9 @@ type emptyBlueprintStateProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const BlueprintsSidebar = () => {
|
const BlueprintsSidebar = () => {
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
const { isFedoraEnv } = useGetEnvironment();
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
|
||||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||||
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
||||||
|
|
@ -73,6 +77,13 @@ const BlueprintsSidebar = () => {
|
||||||
offset: blueprintsOffset,
|
offset: blueprintsOffset,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth?.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
}, [auth]);
|
||||||
|
|
||||||
if (blueprintSearchInput) {
|
if (blueprintSearchInput) {
|
||||||
searchParams.search = blueprintSearchInput;
|
searchParams.search = blueprintSearchInput;
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +101,7 @@ const BlueprintsSidebar = () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Bullseye>
|
<Bullseye>
|
||||||
<Spinner size='xl' />
|
<Spinner size="xl" />
|
||||||
</Bullseye>
|
</Bullseye>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -104,8 +115,8 @@ const BlueprintsSidebar = () => {
|
||||||
<EmptyBlueprintState
|
<EmptyBlueprintState
|
||||||
icon={PlusCircleIcon}
|
icon={PlusCircleIcon}
|
||||||
action={<Link to={resolveRelPath('imagewizard')}>Add blueprint</Link>}
|
action={<Link to={resolveRelPath('imagewizard')}>Add blueprint</Link>}
|
||||||
titleText='No blueprints yet'
|
titleText="No blueprints yet"
|
||||||
bodyText='Add a blueprint and optionally build related images.'
|
bodyText="Add a blueprint and optionally build related images."
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -115,8 +126,8 @@ const BlueprintsSidebar = () => {
|
||||||
dispatch(setBlueprintId(undefined));
|
dispatch(setBlueprintId(undefined));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
if (!process.env.IS_ON_PREMISE && !isFedoraEnv) {
|
||||||
const orgId = userData?.identity.internal?.org_id;
|
const orgId = userData?.identity?.internal?.org_id;
|
||||||
|
|
||||||
analytics.group(orgId, {
|
analytics.group(orgId, {
|
||||||
imagebuilder_blueprint_count: blueprintsData?.meta.count,
|
imagebuilder_blueprint_count: blueprintsData?.meta.count,
|
||||||
|
|
@ -137,7 +148,7 @@ const BlueprintsSidebar = () => {
|
||||||
<Flex justifyContent={{ default: 'justifyContentCenter' }}>
|
<Flex justifyContent={{ default: 'justifyContentCenter' }}>
|
||||||
<FlexItem>
|
<FlexItem>
|
||||||
<Button
|
<Button
|
||||||
variant='link'
|
variant="link"
|
||||||
isDisabled={!selectedBlueprintId}
|
isDisabled={!selectedBlueprintId}
|
||||||
onClick={handleClickViewAll}
|
onClick={handleClickViewAll}
|
||||||
>
|
>
|
||||||
|
|
@ -153,14 +164,14 @@ const BlueprintsSidebar = () => {
|
||||||
icon={SearchIcon}
|
icon={SearchIcon}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
variant='link'
|
variant="link"
|
||||||
onClick={() => dispatch(setBlueprintSearchInput(undefined))}
|
onClick={() => dispatch(setBlueprintSearchInput(undefined))}
|
||||||
>
|
>
|
||||||
Clear all filters
|
Clear all filters
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
titleText='No blueprints found'
|
titleText="No blueprints found"
|
||||||
bodyText='No blueprints match your search criteria. Try a different search.'
|
bodyText="No blueprints match your search criteria. Try a different search."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{blueprintsTotal > 0 &&
|
{blueprintsTotal > 0 &&
|
||||||
|
|
@ -184,7 +195,7 @@ const BlueprintSearch = ({ blueprintsTotal }: blueprintSearchProps) => {
|
||||||
dispatch(imageBuilderApi.util.invalidateTags([{ type: 'Blueprints' }]));
|
dispatch(imageBuilderApi.util.invalidateTags([{ type: 'Blueprints' }]));
|
||||||
dispatch(setBlueprintSearchInput(filter.length > 0 ? filter : undefined));
|
dispatch(setBlueprintSearchInput(filter.length > 0 ? filter : undefined));
|
||||||
}, DEBOUNCED_SEARCH_WAIT_TIME),
|
}, DEBOUNCED_SEARCH_WAIT_TIME),
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -202,7 +213,7 @@ const BlueprintSearch = ({ blueprintsTotal }: blueprintSearchProps) => {
|
||||||
return (
|
return (
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={blueprintSearchInput || ''}
|
value={blueprintSearchInput || ''}
|
||||||
placeholder='Search by name or description'
|
placeholder="Search by name or description"
|
||||||
onChange={(_event, value) => onChange(value)}
|
onChange={(_event, value) => onChange(value)}
|
||||||
onClear={() => onChange('')}
|
onClear={() => onChange('')}
|
||||||
resultsCount={`${blueprintsTotal} blueprints`}
|
resultsCount={`${blueprintsTotal} blueprints`}
|
||||||
|
|
@ -216,7 +227,12 @@ const EmptyBlueprintState = ({
|
||||||
icon,
|
icon,
|
||||||
action,
|
action,
|
||||||
}: emptyBlueprintStateProps) => (
|
}: emptyBlueprintStateProps) => (
|
||||||
<EmptyState headingLevel='h4' icon={icon} titleText={titleText} variant='sm'>
|
<EmptyState variant="sm">
|
||||||
|
<EmptyStateHeader
|
||||||
|
titleText={titleText}
|
||||||
|
headingLevel="h4"
|
||||||
|
icon={<EmptyStateIcon icon={icon} />}
|
||||||
|
/>
|
||||||
<EmptyStateBody>{bodyText}</EmptyStateBody>
|
<EmptyStateBody>{bodyText}</EmptyStateBody>
|
||||||
<EmptyStateFooter>
|
<EmptyStateFooter>
|
||||||
<EmptyStateActions>{action}</EmptyStateActions>
|
<EmptyStateActions>{action}</EmptyStateActions>
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,32 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
ButtonProps,
|
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Flex,
|
MenuToggle,
|
||||||
FlexItem,
|
|
||||||
Menu,
|
Menu,
|
||||||
MenuContent,
|
MenuContent,
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
MenuList,
|
||||||
MenuToggle,
|
MenuItem,
|
||||||
MenuToggleAction,
|
Flex,
|
||||||
|
FlexItem,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
MenuToggleAction,
|
||||||
|
ButtonProps,
|
||||||
|
Button,
|
||||||
} 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 { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
|
|
||||||
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
|
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
|
||||||
import {
|
import {
|
||||||
useComposeBPWithNotification as useComposeBlueprintMutation,
|
useGetBlueprintQuery,
|
||||||
useGetUser,
|
useComposeBlueprintMutation,
|
||||||
} from '../../Hooks';
|
} from '../../store/backendApi';
|
||||||
import { useGetBlueprintQuery } from '../../store/backendApi';
|
|
||||||
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
|
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
|
||||||
import { useAppSelector } from '../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||||
import { ImageTypes } from '../../store/imageBuilderApi';
|
import { ImageTypes } from '../../store/imageBuilderApi';
|
||||||
|
|
||||||
type BuildImagesButtonPropTypes = {
|
type BuildImagesButtonPropTypes = {
|
||||||
|
|
@ -36,27 +37,44 @@ type BuildImagesButtonPropTypes = {
|
||||||
export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
||||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||||
const [deselectedTargets, setDeselectedTargets] = useState<ImageTypes[]>([]);
|
const [deselectedTargets, setDeselectedTargets] = useState<ImageTypes[]>([]);
|
||||||
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
const [buildBlueprint, { isLoading: imageBuildLoading }] =
|
||||||
useComposeBlueprintMutation();
|
useComposeBlueprintMutation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth?.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
}, [auth]);
|
||||||
|
|
||||||
const onBuildHandler = async () => {
|
const onBuildHandler = async () => {
|
||||||
if (selectedBlueprintId) {
|
if (selectedBlueprintId) {
|
||||||
await buildBlueprint({
|
try {
|
||||||
id: selectedBlueprintId,
|
await buildBlueprint({
|
||||||
body: {
|
id: selectedBlueprintId,
|
||||||
image_types: blueprintImageType?.filter(
|
body: {
|
||||||
(target) => !deselectedTargets.includes(target),
|
image_types: blueprintImageType?.filter(
|
||||||
),
|
(target) => !deselectedTargets.includes(target)
|
||||||
},
|
),
|
||||||
});
|
},
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
});
|
||||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Requested`, {
|
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Requested`, {
|
||||||
module: AMPLITUDE_MODULE_NAME,
|
module: AMPLITUDE_MODULE_NAME,
|
||||||
trigger: 'synchronize images',
|
trigger: 'synchronize images',
|
||||||
account_id: userData?.identity.internal?.account_id || 'Not found',
|
account_id: userData?.identity.internal?.account_id || 'Not found',
|
||||||
});
|
});
|
||||||
|
} catch (imageBuildError) {
|
||||||
|
dispatch(
|
||||||
|
addNotification({
|
||||||
|
variant: 'warning',
|
||||||
|
title: 'No blueprint was build',
|
||||||
|
description: imageBuildError?.data?.error?.message,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -65,21 +83,21 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
};
|
};
|
||||||
const { data: blueprintDetails } = useGetBlueprintQuery(
|
const { data: blueprintDetails } = useGetBlueprintQuery(
|
||||||
selectedBlueprintId ? { id: selectedBlueprintId } : skipToken,
|
selectedBlueprintId ? { id: selectedBlueprintId } : skipToken
|
||||||
);
|
);
|
||||||
const blueprintImageType = blueprintDetails?.image_requests.map(
|
const blueprintImageType = blueprintDetails?.image_requests.map(
|
||||||
(image) => image.image_type,
|
(image) => image.image_type
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelect = (
|
const onSelect = (
|
||||||
_event: React.MouseEvent<Element, MouseEvent>,
|
_event: React.MouseEvent<Element, MouseEvent>,
|
||||||
itemId: number,
|
itemId: number
|
||||||
) => {
|
) => {
|
||||||
const imageType = blueprintImageType?.[itemId];
|
const imageType = blueprintImageType?.[itemId];
|
||||||
|
|
||||||
if (imageType && deselectedTargets.includes(imageType)) {
|
if (imageType && deselectedTargets.includes(imageType)) {
|
||||||
setDeselectedTargets(
|
setDeselectedTargets(
|
||||||
deselectedTargets.filter((target) => target !== imageType),
|
deselectedTargets.filter((target) => target !== imageType)
|
||||||
);
|
);
|
||||||
} else if (imageType) {
|
} else if (imageType) {
|
||||||
setDeselectedTargets([...deselectedTargets, imageType]);
|
setDeselectedTargets([...deselectedTargets, imageType]);
|
||||||
|
|
@ -92,40 +110,43 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
||||||
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
|
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
|
||||||
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
|
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
|
||||||
<MenuToggle
|
<MenuToggle
|
||||||
variant='primary'
|
variant="primary"
|
||||||
data-testid='blueprint-build-image-menu'
|
data-testid="blueprint-build-image-menu"
|
||||||
ref={toggleRef}
|
ref={toggleRef}
|
||||||
onClick={onToggleClick}
|
onClick={onToggleClick}
|
||||||
isExpanded={isOpen}
|
isExpanded={isOpen}
|
||||||
splitButtonItems={[
|
splitButtonOptions={{
|
||||||
<MenuToggleAction
|
variant: 'action',
|
||||||
data-testid='blueprint-build-image-menu-option'
|
items: [
|
||||||
key='split-action'
|
<MenuToggleAction
|
||||||
onClick={onBuildHandler}
|
data-testid="blueprint-build-image-menu-option"
|
||||||
id='wizard-build-image-btn'
|
key="split-action"
|
||||||
isDisabled={
|
onClick={onBuildHandler}
|
||||||
!selectedBlueprintId ||
|
id="wizard-build-image-btn"
|
||||||
deselectedTargets.length === blueprintImageType?.length
|
isDisabled={
|
||||||
}
|
!selectedBlueprintId ||
|
||||||
>
|
deselectedTargets.length === blueprintImageType?.length
|
||||||
<Flex display={{ default: 'inlineFlex' }}>
|
}
|
||||||
{imageBuildLoading && (
|
>
|
||||||
<FlexItem>
|
<Flex display={{ default: 'inlineFlex' }}>
|
||||||
<Spinner
|
{imageBuildLoading && (
|
||||||
style={
|
<FlexItem>
|
||||||
{
|
<Spinner
|
||||||
'--pf-v6-c-spinner--Color': '#fff',
|
style={
|
||||||
} as React.CSSProperties
|
{
|
||||||
}
|
'--pf-v5-c-spinner--Color': '#fff',
|
||||||
isInline
|
} as React.CSSProperties
|
||||||
size='md'
|
}
|
||||||
/>
|
isInline
|
||||||
</FlexItem>
|
size="md"
|
||||||
)}
|
/>
|
||||||
<FlexItem>{children ? children : 'Build images'}</FlexItem>
|
</FlexItem>
|
||||||
</Flex>
|
)}
|
||||||
</MenuToggleAction>,
|
<FlexItem>{children ? children : 'Build images'}</FlexItem>
|
||||||
]}
|
</Flex>
|
||||||
|
</MenuToggleAction>,
|
||||||
|
],
|
||||||
|
}}
|
||||||
></MenuToggle>
|
></MenuToggle>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -162,7 +183,7 @@ export const BuildImagesButtonEmptyState = ({
|
||||||
children,
|
children,
|
||||||
}: BuildImagesButtonEmptyStatePropTypes) => {
|
}: BuildImagesButtonEmptyStatePropTypes) => {
|
||||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||||
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
const [buildBlueprint, { isLoading: imageBuildLoading }] =
|
||||||
useComposeBlueprintMutation();
|
useComposeBlueprintMutation();
|
||||||
const onBuildHandler = async () => {
|
const onBuildHandler = async () => {
|
||||||
if (selectedBlueprintId) {
|
if (selectedBlueprintId) {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ActionGroup,
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
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,
|
||||||
|
|
@ -16,10 +15,10 @@ import {
|
||||||
PAGINATION_OFFSET,
|
PAGINATION_OFFSET,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import {
|
import {
|
||||||
useDeleteBPWithNotification as useDeleteBlueprintMutation,
|
backendApi,
|
||||||
useGetUser,
|
useDeleteBlueprintMutation,
|
||||||
} from '../../Hooks';
|
useGetBlueprintsQuery,
|
||||||
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
} from '../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
selectBlueprintSearchInput,
|
selectBlueprintSearchInput,
|
||||||
selectLimit,
|
selectLimit,
|
||||||
|
|
@ -44,7 +43,14 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
||||||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth?.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
}, [auth]);
|
||||||
|
|
||||||
const searchParams: GetBlueprintsApiArg = {
|
const searchParams: GetBlueprintsApiArg = {
|
||||||
limit: blueprintsLimit,
|
limit: blueprintsLimit,
|
||||||
|
|
@ -59,21 +65,19 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
||||||
selectFromResult: ({ data }) => ({
|
selectFromResult: ({ data }) => ({
|
||||||
blueprintName: data?.data.find(
|
blueprintName: data?.data.find(
|
||||||
(blueprint: { id: string | undefined }) =>
|
(blueprint: { id: string | undefined }) =>
|
||||||
blueprint.id === selectedBlueprintId,
|
blueprint.id === selectedBlueprintId
|
||||||
)?.name,
|
)?.name,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const { trigger: deleteBlueprint } = useDeleteBlueprintMutation({
|
const [deleteBlueprint] = useDeleteBlueprintMutation({
|
||||||
fixedCacheKey: 'delete-blueprint',
|
fixedCacheKey: 'delete-blueprint',
|
||||||
});
|
});
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (selectedBlueprintId) {
|
if (selectedBlueprintId) {
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Deleted`, {
|
||||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Deleted`, {
|
module: AMPLITUDE_MODULE_NAME,
|
||||||
module: AMPLITUDE_MODULE_NAME,
|
account_id: userData?.identity.internal?.account_id || 'Not found',
|
||||||
account_id: userData?.identity.internal?.account_id || 'Not found',
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
setShowDeleteModal(false);
|
setShowDeleteModal(false);
|
||||||
await deleteBlueprint({ id: selectedBlueprintId });
|
await deleteBlueprint({ id: selectedBlueprintId });
|
||||||
dispatch(setBlueprintId(undefined));
|
dispatch(setBlueprintId(undefined));
|
||||||
|
|
@ -84,20 +88,22 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
||||||
setShowDeleteModal(false);
|
setShowDeleteModal(false);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Modal variant={ModalVariant.small} isOpen={isOpen} onClose={onDeleteClose}>
|
<Modal
|
||||||
<ModalHeader title={'Delete blueprint?'} titleIconVariant='warning' />
|
variant={ModalVariant.small}
|
||||||
<ModalBody>
|
titleIconVariant="warning"
|
||||||
All versions of {blueprintName} and its associated images will be
|
isOpen={isOpen}
|
||||||
deleted.
|
onClose={onDeleteClose}
|
||||||
</ModalBody>
|
title={'Delete blueprint?'}
|
||||||
<ModalFooter>
|
description={`All versions of ${blueprintName} and its associated images will be deleted.`}
|
||||||
<Button variant='danger' type='button' onClick={handleDelete}>
|
>
|
||||||
|
<ActionGroup>
|
||||||
|
<Button variant="danger" type="button" onClick={handleDelete}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='link' type='button' onClick={onDeleteClose}>
|
<Button variant="link" type="button" onClick={onDeleteClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ActionGroup>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export const EditBlueprintButton = () => {
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(resolveRelPath(`imagewizard/${selectedBlueprintId}`))
|
navigate(resolveRelPath(`imagewizard/${selectedBlueprintId}`))
|
||||||
}
|
}
|
||||||
variant='secondary'
|
variant="secondary"
|
||||||
>
|
>
|
||||||
Edit blueprint
|
Edit blueprint
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { parse } from '@ltd/j-toml';
|
import { parse } from '@ltd/j-toml';
|
||||||
import {
|
import {
|
||||||
|
ActionGroup,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
FileUpload,
|
FileUpload,
|
||||||
|
|
@ -11,15 +12,12 @@ import {
|
||||||
HelperText,
|
HelperText,
|
||||||
HelperTextItem,
|
HelperTextItem,
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalVariant,
|
ModalVariant,
|
||||||
Popover,
|
Popover,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { DropEvent } from '@patternfly/react-core/dist/esm/helpers';
|
import { DropEvent } from '@patternfly/react-core/dist/esm/helpers';
|
||||||
import { HelpIcon } from '@patternfly/react-icons';
|
import { HelpIcon } from '@patternfly/react-icons';
|
||||||
import { useAddNotification } from '@redhat-cloud-services/frontend-components-notifications/hooks';
|
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { mapOnPremToHosted } from './helpers/onPremToHostedBlueprintMapper';
|
import { mapOnPremToHosted } from './helpers/onPremToHostedBlueprintMapper';
|
||||||
|
|
@ -29,6 +27,7 @@ import {
|
||||||
ApiRepositoryRequest,
|
ApiRepositoryRequest,
|
||||||
useBulkImportRepositoriesMutation,
|
useBulkImportRepositoriesMutation,
|
||||||
} from '../../store/contentSourcesApi';
|
} from '../../store/contentSourcesApi';
|
||||||
|
import { useAppDispatch } from '../../store/hooks';
|
||||||
import {
|
import {
|
||||||
BlueprintExportResponse,
|
BlueprintExportResponse,
|
||||||
BlueprintItem,
|
BlueprintItem,
|
||||||
|
|
@ -66,24 +65,24 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
||||||
const [isRejected, setIsRejected] = React.useState(false);
|
const [isRejected, setIsRejected] = React.useState(false);
|
||||||
const [isOnPrem, setIsOnPrem] = React.useState(false);
|
const [isOnPrem, setIsOnPrem] = React.useState(false);
|
||||||
const [isCheckedImportRepos, setIsCheckedImportRepos] = React.useState(true);
|
const [isCheckedImportRepos, setIsCheckedImportRepos] = React.useState(true);
|
||||||
const addNotification = useAddNotification();
|
const dispatch = useAppDispatch();
|
||||||
const [importRepositories] = useBulkImportRepositoriesMutation();
|
const [importRepositories] = useBulkImportRepositoriesMutation();
|
||||||
|
|
||||||
const handleFileInputChange = (
|
const handleFileInputChange = (
|
||||||
_event: React.ChangeEvent<HTMLInputElement> | React.DragEvent<HTMLElement>,
|
_event: React.ChangeEvent<HTMLInputElement> | React.DragEvent<HTMLElement>,
|
||||||
file: File,
|
file: File
|
||||||
) => {
|
) => {
|
||||||
setFileContent('');
|
setFileContent('');
|
||||||
setFilename(file.name);
|
setFilename(file.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleRepositoryImport(
|
async function handleRepositoryImport(
|
||||||
blueprintExportedResponse: BlueprintExportResponse,
|
blueprintExportedResponse: BlueprintExportResponse
|
||||||
): Promise<CustomRepository[] | undefined> {
|
): Promise<CustomRepository[] | undefined> {
|
||||||
if (isCheckedImportRepos && blueprintExportedResponse.content_sources) {
|
if (isCheckedImportRepos && blueprintExportedResponse.content_sources) {
|
||||||
const customRepositories: ApiRepositoryRequest[] =
|
const customRepositories: ApiRepositoryRequest[] =
|
||||||
blueprintExportedResponse.content_sources.map(
|
blueprintExportedResponse.content_sources.map(
|
||||||
(item) => item as ApiRepositoryRequest,
|
(item) => item as ApiRepositoryRequest
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -98,34 +97,40 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
||||||
repository as ApiRepositoryImportResponseRead;
|
repository as ApiRepositoryImportResponseRead;
|
||||||
if (contentSourcesRepo.uuid) {
|
if (contentSourcesRepo.uuid) {
|
||||||
newCustomRepos.push(
|
newCustomRepos.push(
|
||||||
...mapToCustomRepositories(contentSourcesRepo),
|
...mapToCustomRepositories(contentSourcesRepo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (repository.warnings?.length === 0 && repository.url) {
|
if (repository.warnings?.length === 0 && repository.url) {
|
||||||
importedRepositoryNames.push(repository.url);
|
importedRepositoryNames.push(repository.url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addNotification({
|
dispatch(
|
||||||
variant: 'warning',
|
addNotification({
|
||||||
title: 'Failed to import custom repositories',
|
variant: 'warning',
|
||||||
description: JSON.stringify(repository.warnings),
|
title: 'Failed to import custom repositories',
|
||||||
});
|
description: JSON.stringify(repository.warnings),
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (importedRepositoryNames.length !== 0) {
|
if (importedRepositoryNames.length !== 0) {
|
||||||
addNotification({
|
dispatch(
|
||||||
variant: 'info',
|
addNotification({
|
||||||
title: 'Successfully imported custom repositories',
|
variant: 'info',
|
||||||
description: importedRepositoryNames.join(', '),
|
title: 'Successfully imported custom repositories',
|
||||||
});
|
description: importedRepositoryNames.join(', '),
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return newCustomRepos;
|
return newCustomRepos;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
addNotification({
|
dispatch(
|
||||||
variant: 'danger',
|
addNotification({
|
||||||
title: 'Custom repositories import failed',
|
variant: 'danger',
|
||||||
});
|
title: 'Custom repositories import failed',
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,11 +144,11 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
||||||
if (isToml) {
|
if (isToml) {
|
||||||
const tomlBlueprint = parse(fileContent);
|
const tomlBlueprint = parse(fileContent);
|
||||||
const blueprintFromFile = mapOnPremToHosted(
|
const blueprintFromFile = mapOnPremToHosted(
|
||||||
tomlBlueprint as BlueprintItem,
|
tomlBlueprint as BlueprintItem
|
||||||
);
|
);
|
||||||
const importBlueprintState = mapExportRequestToState(
|
const importBlueprintState = mapExportRequestToState(
|
||||||
blueprintFromFile,
|
blueprintFromFile,
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
setIsOnPrem(true);
|
setIsOnPrem(true);
|
||||||
setImportedBlueprint(importBlueprintState);
|
setImportedBlueprint(importBlueprintState);
|
||||||
|
|
@ -155,8 +160,9 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
||||||
blueprintFromFile.content_sources &&
|
blueprintFromFile.content_sources &&
|
||||||
blueprintFromFile.content_sources.length > 0
|
blueprintFromFile.content_sources.length > 0
|
||||||
) {
|
) {
|
||||||
const imported =
|
const imported = await handleRepositoryImport(
|
||||||
await handleRepositoryImport(blueprintFromFile);
|
blueprintFromFile
|
||||||
|
);
|
||||||
customRepos = imported ?? [];
|
customRepos = imported ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,7 +180,7 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
||||||
undefined;
|
undefined;
|
||||||
const importBlueprintState = mapExportRequestToState(
|
const importBlueprintState = mapExportRequestToState(
|
||||||
blueprintExportedResponse,
|
blueprintExportedResponse,
|
||||||
blueprintFromFile.image_requests || [],
|
blueprintFromFile.image_requests || []
|
||||||
);
|
);
|
||||||
|
|
||||||
setIsOnPrem(false);
|
setIsOnPrem(false);
|
||||||
|
|
@ -184,7 +190,7 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
||||||
mapOnPremToHosted(blueprintFromFile);
|
mapOnPremToHosted(blueprintFromFile);
|
||||||
const importBlueprintState = mapExportRequestToState(
|
const importBlueprintState = mapExportRequestToState(
|
||||||
blueprintFromFileMapped,
|
blueprintFromFileMapped,
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
setIsOnPrem(true);
|
setIsOnPrem(true);
|
||||||
setImportedBlueprint(importBlueprintState);
|
setImportedBlueprint(importBlueprintState);
|
||||||
|
|
@ -192,11 +198,13 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsInvalidFormat(true);
|
setIsInvalidFormat(true);
|
||||||
addNotification({
|
dispatch(
|
||||||
variant: 'warning',
|
addNotification({
|
||||||
title: 'File is not a valid blueprint',
|
variant: 'warning',
|
||||||
description: error?.data?.error?.message,
|
title: 'File is not a valid blueprint',
|
||||||
});
|
description: error?.data?.error?.message,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
parseAndImport();
|
parseAndImport();
|
||||||
|
|
@ -241,98 +249,95 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
||||||
<Modal
|
<Modal
|
||||||
variant={ModalVariant.medium}
|
variant={ModalVariant.medium}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Import pipeline
|
||||||
|
<Popover
|
||||||
|
bodyContent={
|
||||||
|
<div>
|
||||||
|
You can import the blueprints you created by using the Red Hat
|
||||||
|
image builder into Insights images to create customized images.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
aria-label="About import"
|
||||||
|
className="pf-v5-u-pl-sm"
|
||||||
|
isInline
|
||||||
|
>
|
||||||
|
<HelpIcon />
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
}
|
||||||
onClose={onImportClose}
|
onClose={onImportClose}
|
||||||
>
|
>
|
||||||
<ModalHeader
|
<Form>
|
||||||
title={
|
<FormGroup fieldId="checkbox-import-custom-repositories">
|
||||||
<>
|
<Checkbox
|
||||||
Import pipeline
|
label="Import missing custom repositories after file upload."
|
||||||
<Popover
|
isChecked={isCheckedImportRepos}
|
||||||
bodyContent={
|
onChange={() => setIsCheckedImportRepos((prev) => !prev)}
|
||||||
<div>
|
aria-label="Import Custom Repositories checkbox"
|
||||||
You can import the blueprints you created by using the Red Hat
|
id="checkbox-import-custom-repositories"
|
||||||
image builder into Insights images to create customized
|
name="Import Repositories"
|
||||||
images.
|
/>
|
||||||
</div>
|
</FormGroup>
|
||||||
}
|
<FormGroup fieldId="import-blueprint-file-upload">
|
||||||
>
|
<FileUpload
|
||||||
<Button
|
id="import-blueprint-file-upload"
|
||||||
icon={<HelpIcon />}
|
type="text"
|
||||||
variant='plain'
|
value={fileContent}
|
||||||
aria-label='About import'
|
filename={filename}
|
||||||
className='pf-v6-u-pl-sm'
|
filenamePlaceholder="Drag and drop a file or upload one"
|
||||||
isInline
|
onFileInputChange={handleFileInputChange}
|
||||||
/>
|
onDataChange={handleDataChange}
|
||||||
</Popover>
|
onReadStarted={handleFileReadStarted}
|
||||||
</>
|
onReadFinished={handleFileReadFinished}
|
||||||
}
|
onClearClick={handleClear}
|
||||||
/>
|
isLoading={isLoading}
|
||||||
<ModalBody>
|
isReadOnly={true}
|
||||||
<Form>
|
browseButtonText="Upload"
|
||||||
<FormGroup fieldId='checkbox-import-custom-repositories'>
|
dropzoneProps={{
|
||||||
<Checkbox
|
accept: { 'text/json': ['.json'], 'text/plain': ['.toml'] },
|
||||||
label='Import missing custom repositories after file upload.'
|
maxSize: 512000,
|
||||||
isChecked={isCheckedImportRepos}
|
onDropRejected: handleFileRejected,
|
||||||
onChange={() => setIsCheckedImportRepos((prev) => !prev)}
|
}}
|
||||||
aria-label='Import Custom Repositories checkbox'
|
validated={isRejected || isInvalidFormat ? 'error' : 'default'}
|
||||||
id='checkbox-import-custom-repositories'
|
/>
|
||||||
name='Import Repositories'
|
<FormHelperText>
|
||||||
/>
|
<HelperText>
|
||||||
</FormGroup>
|
<HelperTextItem variant={variantSwitch()}>
|
||||||
<FormGroup fieldId='import-blueprint-file-upload'>
|
{isRejected
|
||||||
<FileUpload
|
? 'Must be a valid Blueprint JSON/TOML file no larger than 512 KB'
|
||||||
id='import-blueprint-file-upload'
|
: isInvalidFormat
|
||||||
type='text'
|
? 'Not compatible with the blueprints format.'
|
||||||
value={fileContent}
|
: isOnPrem
|
||||||
filename={filename}
|
? 'Importing on-premises blueprints is currently in beta. Results may vary.'
|
||||||
filenamePlaceholder='Drag and drop a file or upload one'
|
: 'Upload your blueprint file. Supported formats: JSON, TOML.'}
|
||||||
onFileInputChange={handleFileInputChange}
|
</HelperTextItem>
|
||||||
onDataChange={handleDataChange}
|
</HelperText>
|
||||||
onReadStarted={handleFileReadStarted}
|
</FormHelperText>
|
||||||
onReadFinished={handleFileReadFinished}
|
</FormGroup>
|
||||||
onClearClick={handleClear}
|
<ActionGroup>
|
||||||
isLoading={isLoading}
|
<Button
|
||||||
isReadOnly={true}
|
type="button"
|
||||||
browseButtonText='Upload'
|
isDisabled={isRejected || isInvalidFormat || !fileContent}
|
||||||
dropzoneProps={{
|
onClick={() =>
|
||||||
accept: { 'text/json': ['.json'], 'text/plain': ['.toml'] },
|
navigate(resolveRelPath(`imagewizard/import`), {
|
||||||
maxSize: 512000,
|
state: { blueprint: importedBlueprint },
|
||||||
onDropRejected: handleFileRejected,
|
})
|
||||||
}}
|
}
|
||||||
validated={isRejected || isInvalidFormat ? 'error' : 'default'}
|
data-testid="import-blueprint-finish"
|
||||||
/>
|
>
|
||||||
<FormHelperText>
|
Review and finish
|
||||||
<HelperText>
|
</Button>
|
||||||
<HelperTextItem variant={variantSwitch()}>
|
<Button variant="link" type="button" onClick={onImportClose}>
|
||||||
{isRejected
|
Cancel
|
||||||
? 'Must be a valid Blueprint JSON/TOML file no larger than 512 KB'
|
</Button>
|
||||||
: isInvalidFormat
|
</ActionGroup>
|
||||||
? 'Not compatible with the blueprints format.'
|
</Form>
|
||||||
: isOnPrem
|
|
||||||
? 'Importing on-premises blueprints is currently in beta. Results may vary.'
|
|
||||||
: 'Upload your blueprint file. Supported formats: JSON, TOML.'}
|
|
||||||
</HelperTextItem>
|
|
||||||
</HelperText>
|
|
||||||
</FormHelperText>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
isDisabled={isRejected || isInvalidFormat || !fileContent}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(resolveRelPath(`imagewizard/import`), {
|
|
||||||
state: { blueprint: importedBlueprint },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Review and finish
|
|
||||||
</Button>
|
|
||||||
<Button variant='link' type='button' onClick={onImportClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -101,13 +101,13 @@ export type SshKeyOnPrem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapOnPremToHosted = (
|
export const mapOnPremToHosted = (
|
||||||
blueprint: BlueprintOnPrem,
|
blueprint: BlueprintOnPrem
|
||||||
): BlueprintExportResponse => {
|
): BlueprintExportResponse => {
|
||||||
const users = blueprint.customizations?.user?.map((u) => ({
|
const users = blueprint.customizations?.user?.map((u) => ({
|
||||||
name: u.name,
|
name: u.name,
|
||||||
ssh_key: u.key,
|
ssh_key: u.key,
|
||||||
groups: u.groups,
|
groups: u.groups,
|
||||||
isAdministrator: u.groups.includes('wheel') || false,
|
isAdministrator: u.groups?.includes('wheel') || false,
|
||||||
}));
|
}));
|
||||||
const user_keys = blueprint.customizations?.sshkey?.map((k) => ({
|
const user_keys = blueprint.customizations?.sshkey?.map((k) => ({
|
||||||
name: k.user,
|
name: k.user,
|
||||||
|
|
@ -132,7 +132,7 @@ export const mapOnPremToHosted = (
|
||||||
({ baseurls, ...fs }) => ({
|
({ baseurls, ...fs }) => ({
|
||||||
baseurl: baseurls,
|
baseurl: baseurls,
|
||||||
...fs,
|
...fs,
|
||||||
}),
|
})
|
||||||
),
|
),
|
||||||
packages:
|
packages:
|
||||||
packages !== undefined || groups !== undefined
|
packages !== undefined || groups !== undefined
|
||||||
|
|
@ -147,7 +147,7 @@ export const mapOnPremToHosted = (
|
||||||
({ minsize, ...fs }) => ({
|
({ minsize, ...fs }) => ({
|
||||||
min_size: minsize,
|
min_size: minsize,
|
||||||
...fs,
|
...fs,
|
||||||
}),
|
})
|
||||||
),
|
),
|
||||||
fips:
|
fips:
|
||||||
blueprint.customizations?.fips !== undefined
|
blueprint.customizations?.fips !== undefined
|
||||||
|
|
@ -189,14 +189,14 @@ export const mapOnPremToHosted = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapHostedToOnPrem = (
|
export const mapHostedToOnPrem = (
|
||||||
blueprint: CreateBlueprintRequest,
|
blueprint: CreateBlueprintRequest
|
||||||
): CloudApiBlueprint => {
|
): CloudApiBlueprint => {
|
||||||
const result: CloudApiBlueprint = {
|
const result: CloudApiBlueprint = {
|
||||||
name: blueprint.name,
|
name: blueprint.name,
|
||||||
customizations: {},
|
customizations: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (blueprint.customizations.packages) {
|
if (blueprint.customizations?.packages) {
|
||||||
result.packages = blueprint.customizations.packages.map((pkg) => {
|
result.packages = blueprint.customizations.packages.map((pkg) => {
|
||||||
return {
|
return {
|
||||||
name: pkg,
|
name: pkg,
|
||||||
|
|
@ -205,30 +205,30 @@ export const mapHostedToOnPrem = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.containers) {
|
if (blueprint.customizations?.containers) {
|
||||||
result.containers = blueprint.customizations.containers;
|
result.containers = blueprint.customizations.containers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.directories) {
|
if (blueprint.customizations?.directories) {
|
||||||
result.customizations!.directories = blueprint.customizations.directories;
|
result.customizations!.directories = blueprint.customizations.directories;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.files) {
|
if (blueprint.customizations?.files) {
|
||||||
result.customizations!.files = blueprint.customizations.files;
|
result.customizations!.files = blueprint.customizations.files;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.filesystem) {
|
if (blueprint.customizations?.filesystem) {
|
||||||
result.customizations!.filesystem = blueprint.customizations.filesystem.map(
|
result.customizations!.filesystem = blueprint.customizations.filesystem.map(
|
||||||
(fs) => {
|
(fs) => {
|
||||||
return {
|
return {
|
||||||
mountpoint: fs.mountpoint,
|
mountpoint: fs.mountpoint,
|
||||||
minsize: fs.min_size,
|
minsize: fs.min_size,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.users) {
|
if (blueprint.customizations?.users) {
|
||||||
result.customizations!.user = blueprint.customizations.users.map((u) => {
|
result.customizations!.user = blueprint.customizations.users.map((u) => {
|
||||||
return {
|
return {
|
||||||
name: u.name,
|
name: u.name,
|
||||||
|
|
@ -239,54 +239,54 @@ export const mapHostedToOnPrem = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.services) {
|
if (blueprint.customizations?.services) {
|
||||||
result.customizations!.services = blueprint.customizations.services;
|
result.customizations!.services = blueprint.customizations.services;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.hostname) {
|
if (blueprint.customizations?.hostname) {
|
||||||
result.customizations!.hostname = blueprint.customizations.hostname;
|
result.customizations!.hostname = blueprint.customizations.hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.kernel) {
|
if (blueprint.customizations?.kernel) {
|
||||||
result.customizations!.kernel = blueprint.customizations.kernel;
|
result.customizations!.kernel = blueprint.customizations.kernel;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.timezone) {
|
if (blueprint.customizations?.timezone) {
|
||||||
result.customizations!.timezone = blueprint.customizations.timezone;
|
result.customizations!.timezone = blueprint.customizations.timezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.locale) {
|
if (blueprint.customizations?.locale) {
|
||||||
result.customizations!.locale = blueprint.customizations.locale;
|
result.customizations!.locale = blueprint.customizations.locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.firewall) {
|
if (blueprint.customizations?.firewall) {
|
||||||
result.customizations!.firewall = blueprint.customizations.firewall;
|
result.customizations!.firewall = blueprint.customizations.firewall;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.installation_device) {
|
if (blueprint.customizations?.installation_device) {
|
||||||
result.customizations!.installation_device =
|
result.customizations!.installation_device =
|
||||||
blueprint.customizations.installation_device;
|
blueprint.customizations.installation_device;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.fdo) {
|
if (blueprint.customizations?.fdo) {
|
||||||
result.customizations!.fdo = blueprint.customizations.fdo;
|
result.customizations!.fdo = blueprint.customizations.fdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.ignition) {
|
if (blueprint.customizations?.ignition) {
|
||||||
result.customizations!.ignition = blueprint.customizations.ignition;
|
result.customizations!.ignition = blueprint.customizations.ignition;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.partitioning_mode) {
|
if (blueprint.customizations?.partitioning_mode) {
|
||||||
result.customizations!.partitioning_mode =
|
result.customizations!.partitioning_mode =
|
||||||
blueprint.customizations.partitioning_mode;
|
blueprint.customizations.partitioning_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.fips) {
|
if (blueprint.customizations?.fips) {
|
||||||
result.customizations!.fips =
|
result.customizations!.fips =
|
||||||
blueprint.customizations.fips.enabled || false;
|
blueprint.customizations.fips?.enabled || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blueprint.customizations.installer) {
|
if (blueprint.customizations?.installer) {
|
||||||
result.customizations!.installer = blueprint.customizations.installer;
|
result.customizations!.installer = blueprint.customizations.installer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Content,
|
|
||||||
Form,
|
|
||||||
FormGroup,
|
|
||||||
Popover,
|
|
||||||
Switch,
|
|
||||||
TextInput,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { HelpIcon } from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
import { isAwsBucketValid, isAwsCredsPathValid } from './validators';
|
|
||||||
|
|
||||||
import {
|
|
||||||
changeAWSBucketName,
|
|
||||||
changeAWSCredsPath,
|
|
||||||
reinitializeAWSConfig,
|
|
||||||
selectAWSBucketName,
|
|
||||||
selectAWSCredsPath,
|
|
||||||
} from '../../store/cloudProviderConfigSlice';
|
|
||||||
import {
|
|
||||||
AWSWorkerConfig,
|
|
||||||
WorkerConfigResponse,
|
|
||||||
} from '../../store/cockpit/types';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
|
||||||
import { ValidatedInput } from '../CreateImageWizard/ValidatedInput';
|
|
||||||
|
|
||||||
type FormGroupProps<T> = {
|
|
||||||
value: T | undefined;
|
|
||||||
onChange: (value: T) => void;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ToggleGroupProps = Omit<FormGroupProps<boolean>, 'isDisabled'>;
|
|
||||||
|
|
||||||
const AWSConfigToggle = ({ value, onChange }: ToggleGroupProps) => {
|
|
||||||
const handleChange = (
|
|
||||||
_event: React.FormEvent<HTMLInputElement>,
|
|
||||||
checked: boolean,
|
|
||||||
) => {
|
|
||||||
onChange(checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup label='Configure AWS Uploads'>
|
|
||||||
<Switch
|
|
||||||
id='aws-config-switch'
|
|
||||||
ouiaId='aws-config-switch'
|
|
||||||
aria-label='aws-config-switch'
|
|
||||||
// empty label so there is no icon
|
|
||||||
label=''
|
|
||||||
isChecked={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DisabledInputGroup = ({
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
ariaLabel,
|
|
||||||
}: {
|
|
||||||
value: string | undefined;
|
|
||||||
label: React.ReactNode;
|
|
||||||
ariaLabel: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<FormGroup label={label}>
|
|
||||||
<TextInput aria-label={ariaLabel} value={value || ''} isDisabled />
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AWSBucket = ({ value, onChange, isDisabled }: FormGroupProps<string>) => {
|
|
||||||
const label = 'AWS Bucket';
|
|
||||||
|
|
||||||
if (isDisabled) {
|
|
||||||
return (
|
|
||||||
<DisabledInputGroup label={label} value={value} ariaLabel='aws-bucket' />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup label={label}>
|
|
||||||
<ValidatedInput
|
|
||||||
placeholder='AWS bucket'
|
|
||||||
ariaLabel='aws-bucket'
|
|
||||||
value={value || ''}
|
|
||||||
validator={isAwsBucketValid}
|
|
||||||
onChange={(_event, value) => onChange(value)}
|
|
||||||
helperText='Invalid AWS bucket name'
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CredsPathPopover = () => {
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
minWidth='35rem'
|
|
||||||
headerContent={'What is the AWS Credentials Path?'}
|
|
||||||
bodyContent={
|
|
||||||
<Content>
|
|
||||||
<Content>
|
|
||||||
This is the path to your AWS credentials file which contains your
|
|
||||||
aws access key id and secret access key. This path to the file is
|
|
||||||
normally in the home directory in the credentials file in the .aws
|
|
||||||
directory, <br /> i.e. /home/USERNAME/.aws/credentials
|
|
||||||
</Content>
|
|
||||||
</Content>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon={<HelpIcon />}
|
|
||||||
variant='plain'
|
|
||||||
aria-label='Credentials Path Info'
|
|
||||||
className='pf-v6-u-pl-sm header-button'
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AWSCredsPath = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
isDisabled,
|
|
||||||
}: FormGroupProps<string>) => {
|
|
||||||
const label = (
|
|
||||||
<>
|
|
||||||
AWS Credentials Filepath <CredsPathPopover />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDisabled) {
|
|
||||||
return (
|
|
||||||
<DisabledInputGroup
|
|
||||||
value={value}
|
|
||||||
label={label}
|
|
||||||
ariaLabel='aws-creds-path'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup label={label}>
|
|
||||||
<ValidatedInput
|
|
||||||
placeholder='Path to AWS credentials'
|
|
||||||
ariaLabel='aws-creds-path'
|
|
||||||
value={value || ''}
|
|
||||||
validator={isAwsCredsPathValid}
|
|
||||||
onChange={(_event, value) => onChange(value)}
|
|
||||||
helperText='Invalid filepath for AWS credentials'
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type AWSConfigProps = {
|
|
||||||
enabled: boolean;
|
|
||||||
setEnabled: (enabled: boolean) => void;
|
|
||||||
reinit: (config: AWSWorkerConfig | undefined) => void;
|
|
||||||
refetch: () => Promise<{
|
|
||||||
data?: WorkerConfigResponse | undefined;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AWSConfig = ({
|
|
||||||
enabled,
|
|
||||||
setEnabled,
|
|
||||||
refetch,
|
|
||||||
reinit,
|
|
||||||
}: AWSConfigProps) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const bucket = useAppSelector(selectAWSBucketName);
|
|
||||||
const credentials = useAppSelector(selectAWSCredsPath);
|
|
||||||
|
|
||||||
const onToggle = async (v: boolean) => {
|
|
||||||
if (v) {
|
|
||||||
try {
|
|
||||||
const { data } = await refetch();
|
|
||||||
reinit(data?.aws);
|
|
||||||
setEnabled(v);
|
|
||||||
return;
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dispatch(reinitializeAWSConfig());
|
|
||||||
setEnabled(v);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form>
|
|
||||||
<AWSConfigToggle value={enabled} onChange={onToggle} />
|
|
||||||
<AWSBucket
|
|
||||||
value={bucket}
|
|
||||||
onChange={(v) => dispatch(changeAWSBucketName(v))}
|
|
||||||
isDisabled={!enabled}
|
|
||||||
/>
|
|
||||||
<AWSCredsPath
|
|
||||||
value={credentials}
|
|
||||||
onChange={(v) => dispatch(changeAWSCredsPath(v))}
|
|
||||||
isDisabled={!enabled}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
import React, {
|
|
||||||
MouseEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
EmptyState,
|
|
||||||
EmptyStateActions,
|
|
||||||
EmptyStateBody,
|
|
||||||
EmptyStateFooter,
|
|
||||||
EmptyStateVariant,
|
|
||||||
PageSection,
|
|
||||||
Skeleton,
|
|
||||||
Title,
|
|
||||||
Wizard,
|
|
||||||
WizardStep,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { ExclamationIcon } from '@patternfly/react-icons';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { AWSConfig } from './AWSConfig';
|
|
||||||
import { isAwsStepValid } from './validators';
|
|
||||||
|
|
||||||
import {
|
|
||||||
changeAWSBucketName,
|
|
||||||
changeAWSCredsPath,
|
|
||||||
reinitializeAWSConfig,
|
|
||||||
selectAWSConfig,
|
|
||||||
} from '../../store/cloudProviderConfigSlice';
|
|
||||||
import {
|
|
||||||
useGetWorkerConfigQuery,
|
|
||||||
useUpdateWorkerConfigMutation,
|
|
||||||
} from '../../store/cockpit/cockpitApi';
|
|
||||||
import { AWSWorkerConfig } from '../../store/cockpit/types';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
|
||||||
import { resolveRelPath } from '../../Utilities/path';
|
|
||||||
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
|
|
||||||
|
|
||||||
const ConfigError = ({
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
onClose: MouseEventHandler<HTMLButtonElement>;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<EmptyState
|
|
||||||
variant={EmptyStateVariant.xl}
|
|
||||||
icon={ExclamationIcon}
|
|
||||||
color='#C9190B'
|
|
||||||
>
|
|
||||||
<Title headingLevel='h4' size='lg'>
|
|
||||||
Error
|
|
||||||
</Title>
|
|
||||||
<EmptyStateBody>
|
|
||||||
There was an error reading the `/etc/osbuild-worker/osbuild-worker.toml`
|
|
||||||
config file
|
|
||||||
</EmptyStateBody>
|
|
||||||
<EmptyStateFooter>
|
|
||||||
<EmptyStateActions>
|
|
||||||
<Button variant='primary' onClick={onClose}>
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
</EmptyStateActions>
|
|
||||||
</EmptyStateFooter>
|
|
||||||
</EmptyState>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CloudProviderConfig = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const config = useAppSelector(selectAWSConfig);
|
|
||||||
const handleClose = () => navigate(resolveRelPath(''));
|
|
||||||
const [enabled, setEnabled] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [updateConfig] = useUpdateWorkerConfigMutation();
|
|
||||||
const { data, error, refetch, isLoading } = useGetWorkerConfigQuery({});
|
|
||||||
|
|
||||||
const initAWSConfig = useCallback(
|
|
||||||
(config: AWSWorkerConfig | undefined) => {
|
|
||||||
if (!config) {
|
|
||||||
dispatch(reinitializeAWSConfig());
|
|
||||||
setEnabled(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEnabled(true);
|
|
||||||
|
|
||||||
const { bucket, credentials } = config;
|
|
||||||
if (bucket && bucket !== '') {
|
|
||||||
dispatch(changeAWSBucketName(bucket));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (credentials && credentials !== '') {
|
|
||||||
dispatch(changeAWSCredsPath(credentials));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch, setEnabled],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initAWSConfig(data?.aws);
|
|
||||||
}, [data, initAWSConfig]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Skeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ConfigError onClose={handleClose} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ImageBuilderHeader inWizard={true} />
|
|
||||||
<PageSection>
|
|
||||||
<Wizard onClose={handleClose}>
|
|
||||||
<WizardStep
|
|
||||||
name='AWS Config'
|
|
||||||
id='aws-config'
|
|
||||||
footer={{
|
|
||||||
nextButtonText: 'Submit',
|
|
||||||
isNextDisabled: !isAwsStepValid(config),
|
|
||||||
isBackDisabled: true,
|
|
||||||
onNext: () => {
|
|
||||||
updateConfig({
|
|
||||||
updateWorkerConfigRequest: { aws: config },
|
|
||||||
});
|
|
||||||
navigate(resolveRelPath(''));
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AWSConfig
|
|
||||||
refetch={refetch}
|
|
||||||
reinit={initAWSConfig}
|
|
||||||
enabled={enabled}
|
|
||||||
setEnabled={setEnabled}
|
|
||||||
/>
|
|
||||||
</WizardStep>
|
|
||||||
</Wizard>
|
|
||||||
</PageSection>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import { AWSWorkerConfig } from '../../../store/cockpit/types';
|
|
||||||
|
|
||||||
export const isAwsBucketValid = (bucket?: string): boolean => {
|
|
||||||
if (!bucket || bucket === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex = /^[a-z0-9](?:[a-z0-9]|[-.](?=[a-z0-9])){1,61}[a-z0-9]$/;
|
|
||||||
return regex.test(bucket);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isAwsCredsPathValid = (credsPath?: string): boolean => {
|
|
||||||
if (!credsPath || credsPath === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validPathPattern = /^(\/[^/\0]*)+\/?$/;
|
|
||||||
return path.isAbsolute(credsPath) && validPathPattern.test(credsPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isAwsStepValid = (
|
|
||||||
config: AWSWorkerConfig | undefined,
|
|
||||||
): boolean => {
|
|
||||||
if (!config) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.bucket && !config.credentials) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
isAwsBucketValid(config.bucket) && isAwsCredsPathValid(config.credentials)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -5,6 +5,8 @@ import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
EmptyStateActions,
|
EmptyStateActions,
|
||||||
EmptyStateFooter,
|
EmptyStateFooter,
|
||||||
|
EmptyStateHeader,
|
||||||
|
EmptyStateIcon,
|
||||||
EmptyStateVariant,
|
EmptyStateVariant,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { CubesIcon } from '@patternfly/react-icons';
|
import { CubesIcon } from '@patternfly/react-icons';
|
||||||
|
|
@ -12,16 +14,16 @@ import cockpit from 'cockpit';
|
||||||
|
|
||||||
export const NotReady = ({ enabled }: { enabled: boolean }) => {
|
export const NotReady = ({ enabled }: { enabled: boolean }) => {
|
||||||
return (
|
return (
|
||||||
<EmptyState
|
<EmptyState variant={EmptyStateVariant.xl}>
|
||||||
headingLevel='h4'
|
<EmptyStateHeader
|
||||||
icon={CubesIcon}
|
titleText={`OSBuild Composer is not ${enabled ? 'started' : 'enabled'}`}
|
||||||
titleText={`OSBuild Composer is not ${enabled ? 'started' : 'enabled'}`}
|
headingLevel="h4"
|
||||||
variant={EmptyStateVariant.xl}
|
icon={<EmptyStateIcon icon={CubesIcon} />}
|
||||||
>
|
/>
|
||||||
<EmptyStateFooter>
|
<EmptyStateFooter>
|
||||||
<EmptyStateActions>
|
<EmptyStateActions>
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant="primary"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
cockpit
|
cockpit
|
||||||
|
|
@ -30,7 +32,7 @@ export const NotReady = ({ enabled }: { enabled: boolean }) => {
|
||||||
{
|
{
|
||||||
superuser: 'require',
|
superuser: 'require',
|
||||||
err: 'message',
|
err: 'message',
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.then(() => window.location.reload());
|
.then(() => window.location.reload());
|
||||||
}}
|
}}
|
||||||
|
|
@ -40,12 +42,12 @@ export const NotReady = ({ enabled }: { enabled: boolean }) => {
|
||||||
</EmptyStateActions>
|
</EmptyStateActions>
|
||||||
<EmptyStateActions>
|
<EmptyStateActions>
|
||||||
<Button
|
<Button
|
||||||
variant='link'
|
variant="link"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
cockpit.jump(
|
cockpit.jump(
|
||||||
'/system/services#/osbuild-composer.socket',
|
'/system/services#/osbuild-composer.socket',
|
||||||
cockpit.transport.host,
|
cockpit.transport.host
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,20 @@ import React from 'react';
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
EmptyStateBody,
|
EmptyStateBody,
|
||||||
|
EmptyStateHeader,
|
||||||
|
EmptyStateIcon,
|
||||||
EmptyStateVariant,
|
EmptyStateVariant,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { LockIcon } from '@patternfly/react-icons';
|
import { LockIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
export const RequireAdmin = () => {
|
export const RequireAdmin = () => {
|
||||||
return (
|
return (
|
||||||
<EmptyState
|
<EmptyState variant={EmptyStateVariant.xl}>
|
||||||
headingLevel='h4'
|
<EmptyStateHeader
|
||||||
icon={LockIcon}
|
titleText="Access is limited."
|
||||||
titleText='Access is limited.'
|
headingLevel="h4"
|
||||||
variant={EmptyStateVariant.xl}
|
icon={<EmptyStateIcon icon={LockIcon} color="#f4c145" />}
|
||||||
>
|
/>
|
||||||
<EmptyStateBody>
|
<EmptyStateBody>
|
||||||
Administrative access is required to run the Image Builder frontend.
|
Administrative access is required to run the Image Builder frontend.
|
||||||
Click on the icon in the toolbar to grant administrative access.
|
Click on the icon in the toolbar to grant administrative access.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
.pf-v6-c-wizard__nav-list {
|
.pf-v5-c-wizard__nav-list {
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10,24 +10,45 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-c-form {
|
.pf-c-form {
|
||||||
--pf-c-form--GridGap: var(--pf6-global--spacer--md);
|
--pf-c-form--GridGap: var(--pf-v5-global--spacer--md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-c-form__group-label {
|
.pf-c-form__group-label {
|
||||||
--pf-c-form__group-label--PaddingBottom: var(--pf-v6-global--spacer--xs);
|
--pf-c-form__group-label--PaddingBottom: var(--pf-v5-global--spacer--xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiles {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
flex: 1 0 0px;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-tile:focus {
|
||||||
|
--pf-c-tile__title--Color: var(--pf-c-tile__title--Color);
|
||||||
|
--pf-c-tile__icon--Color: var(---pf-v5-global--Color--100);
|
||||||
|
--pf-c-tile--before--BorderWidth: var(--pf-v5-global--BorderWidth--sm);
|
||||||
|
--pf-c-tile--before--BorderColor: var(--pf-v5-global--BorderColor--100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-tile.pf-m-selected:focus {
|
||||||
|
--pf-c-tile__title--Color: var(--pf-c-tile--focus__title--Color);
|
||||||
|
--pf-c-tile__icon--Color: var(--pf-c-tile--focus__icon--Color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-icon {
|
.provider-icon {
|
||||||
width: 3.5em;
|
width: 1em;
|
||||||
height: 3.5em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-v6-u-min-width {
|
.pf-v5-u-min-width {
|
||||||
--pf-v6-u-min-width--MinWidth: 18ch;
|
--pf-v5-u-min-width--MinWidth: 18ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-v6-u-max-width {
|
.pf-v5-u-max-width {
|
||||||
--pf-v6-u-max-width--MaxWidth: 26rem;
|
--pf-v5-u-max-width--MaxWidth: 26rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.pf-m-plain {
|
ul.pf-m-plain {
|
||||||
|
|
@ -41,20 +62,15 @@ ul.pf-m-plain {
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-border {
|
.panel-border {
|
||||||
--pf-v6-c-panel--before--BorderColor: #BEE1F4;
|
--pf-v5-c-panel--before--BorderColor: #BEE1F4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Targets the alert within the Reviewsteps > content dropdown
|
// Targets the alert within the Reviewsteps > content dropdown
|
||||||
// Removes excess top margin padding
|
// Removes excess top margin padding
|
||||||
div.pf-v6-c-alert.pf-m-inline.pf-m-plain.pf-m-warning {
|
div.pf-v5-c-alert.pf-m-inline.pf-m-plain.pf-m-warning {
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensures the wizard takes up the entire height of the page in Firefox as well
|
|
||||||
.pf-v6-c-wizard {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,21 @@ import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
|
||||||
PageSection,
|
|
||||||
PageSectionTypes,
|
|
||||||
useWizardContext,
|
|
||||||
Wizard,
|
Wizard,
|
||||||
WizardFooterWrapper,
|
WizardFooterWrapper,
|
||||||
WizardNavItem,
|
WizardNavItem,
|
||||||
WizardStep,
|
WizardStep,
|
||||||
|
useWizardContext,
|
||||||
|
PageSection,
|
||||||
|
PageSectionTypes,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { WizardStepType } from '@patternfly/react-core/dist/esm/components/Wizard';
|
import { WizardStepType } from '@patternfly/react-core/dist/esm/components/Wizard';
|
||||||
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/FileSystemTable';
|
||||||
import FirewallStep from './steps/Firewall';
|
import FirewallStep from './steps/Firewall';
|
||||||
import FirstBootStep from './steps/FirstBoot';
|
import FirstBootStep from './steps/FirstBoot';
|
||||||
import HostnameStep from './steps/Hostname';
|
import HostnameStep from './steps/Hostname';
|
||||||
|
|
@ -39,59 +37,60 @@ import Gcp from './steps/TargetEnvironment/Gcp';
|
||||||
import TimezoneStep from './steps/Timezone';
|
import TimezoneStep from './steps/Timezone';
|
||||||
import UsersStep from './steps/Users';
|
import UsersStep from './steps/Users';
|
||||||
import { getHostArch, getHostDistro } from './utilities/getHostInfo';
|
import { getHostArch, getHostDistro } from './utilities/getHostInfo';
|
||||||
import { useHasSpecificTargetOnly } from './utilities/hasSpecificTargetOnly';
|
|
||||||
import {
|
import {
|
||||||
useAAPValidation,
|
|
||||||
useDetailsValidation,
|
|
||||||
useFilesystemValidation,
|
useFilesystemValidation,
|
||||||
useFirewallValidation,
|
useSnapshotValidation,
|
||||||
useFirstBootValidation,
|
useFirstBootValidation,
|
||||||
|
useDetailsValidation,
|
||||||
|
useRegistrationValidation,
|
||||||
useHostnameValidation,
|
useHostnameValidation,
|
||||||
useKernelValidation,
|
useKernelValidation,
|
||||||
useLocaleValidation,
|
|
||||||
useRegistrationValidation,
|
|
||||||
useServicesValidation,
|
|
||||||
useSnapshotValidation,
|
|
||||||
useTimezoneValidation,
|
|
||||||
useUsersValidation,
|
useUsersValidation,
|
||||||
|
useTimezoneValidation,
|
||||||
|
useFirewallValidation,
|
||||||
|
useServicesValidation,
|
||||||
|
useLocaleValidation,
|
||||||
} from './utilities/useValidation';
|
} from './utilities/useValidation';
|
||||||
import {
|
import {
|
||||||
isAwsAccountIdValid,
|
isAwsAccountIdValid,
|
||||||
isAzureResourceGroupValid,
|
|
||||||
isAzureSubscriptionIdValid,
|
|
||||||
isAzureTenantGUIDValid,
|
isAzureTenantGUIDValid,
|
||||||
|
isAzureSubscriptionIdValid,
|
||||||
|
isAzureResourceGroupValid,
|
||||||
isGcpEmailValid,
|
isGcpEmailValid,
|
||||||
} from './validators';
|
} from './validators';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AARCH64,
|
|
||||||
AMPLITUDE_MODULE_NAME,
|
|
||||||
RHEL_10,
|
|
||||||
RHEL_8,
|
RHEL_8,
|
||||||
RHEL_9,
|
RHEL_10_BETA,
|
||||||
|
RHEL_10,
|
||||||
|
AARCH64,
|
||||||
|
CENTOS_9,
|
||||||
|
AMPLITUDE_MODULE_NAME,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||||
import './CreateImageWizard.scss';
|
import './CreateImageWizard.scss';
|
||||||
import {
|
import {
|
||||||
addImageType,
|
|
||||||
changeArchitecture,
|
|
||||||
changeAwsShareMethod,
|
|
||||||
changeDistribution,
|
changeDistribution,
|
||||||
|
changeArchitecture,
|
||||||
initializeWizard,
|
initializeWizard,
|
||||||
selectAwsAccountId,
|
selectAwsAccountId,
|
||||||
selectAwsShareMethod,
|
selectAwsShareMethod,
|
||||||
selectAwsSourceId,
|
selectAwsSourceId,
|
||||||
selectAzureResourceGroup,
|
selectAzureResourceGroup,
|
||||||
|
selectAzureShareMethod,
|
||||||
|
selectAzureSource,
|
||||||
selectAzureSubscriptionId,
|
selectAzureSubscriptionId,
|
||||||
selectAzureTenantId,
|
selectAzureTenantId,
|
||||||
selectDistribution,
|
selectDistribution,
|
||||||
selectGcpEmail,
|
selectGcpEmail,
|
||||||
selectGcpShareMethod,
|
selectGcpShareMethod,
|
||||||
selectImageTypes,
|
selectImageTypes,
|
||||||
|
addImageType,
|
||||||
|
changeRegistrationType,
|
||||||
} from '../../store/wizardSlice';
|
} from '../../store/wizardSlice';
|
||||||
import isRhel from '../../Utilities/isRhel';
|
import isRhel from '../../Utilities/isRhel';
|
||||||
import { resolveRelPath } from '../../Utilities/path';
|
import { resolveRelPath } from '../../Utilities/path';
|
||||||
import { useFlag } from '../../Utilities/useGetEnvironment';
|
import { useFlag, useGetEnvironment } from '../../Utilities/useGetEnvironment';
|
||||||
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
|
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
|
||||||
|
|
||||||
type CustomWizardFooterPropType = {
|
type CustomWizardFooterPropType = {
|
||||||
|
|
@ -116,73 +115,71 @@ export const CustomWizardFooter = ({
|
||||||
const cancelBtnID = 'wizard-cancel-btn';
|
const cancelBtnID = 'wizard-cancel-btn';
|
||||||
return (
|
return (
|
||||||
<WizardFooterWrapper>
|
<WizardFooterWrapper>
|
||||||
<Flex columnGap={{ default: 'columnGapSm' }}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (!process.env.IS_ON_PREMISE) {
|
||||||
|
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
||||||
|
module: AMPLITUDE_MODULE_NAME,
|
||||||
|
button_id: nextBtnID,
|
||||||
|
active_step_id: activeStep.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!beforeNext || beforeNext()) goToNextStep();
|
||||||
|
}}
|
||||||
|
isDisabled={disableNext}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
if (!process.env.IS_ON_PREMISE) {
|
||||||
|
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
||||||
|
module: AMPLITUDE_MODULE_NAME,
|
||||||
|
button_id: backBtnID,
|
||||||
|
active_step_id: activeStep.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
goToPrevStep();
|
||||||
|
}}
|
||||||
|
isDisabled={disableBack || false}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
{optional && (
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant="tertiary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
if (!process.env.IS_ON_PREMISE) {
|
||||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
||||||
module: AMPLITUDE_MODULE_NAME,
|
module: AMPLITUDE_MODULE_NAME,
|
||||||
button_id: nextBtnID,
|
button_id: reviewAndFinishBtnID,
|
||||||
active_step_id: activeStep.id,
|
active_step_id: activeStep.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!beforeNext || beforeNext()) goToNextStep();
|
if (!beforeNext || beforeNext()) goToStepById('step-review');
|
||||||
}}
|
}}
|
||||||
isDisabled={disableNext}
|
isDisabled={disableNext}
|
||||||
>
|
>
|
||||||
Next
|
Review and finish
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
)}
|
||||||
variant='secondary'
|
<Button
|
||||||
onClick={() => {
|
variant="link"
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
onClick={() => {
|
||||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
if (!process.env.IS_ON_PREMISE) {
|
||||||
module: AMPLITUDE_MODULE_NAME,
|
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
||||||
button_id: backBtnID,
|
module: AMPLITUDE_MODULE_NAME,
|
||||||
active_step_id: activeStep.id,
|
button_id: cancelBtnID,
|
||||||
});
|
active_step_id: activeStep.id,
|
||||||
}
|
});
|
||||||
goToPrevStep();
|
}
|
||||||
}}
|
close();
|
||||||
isDisabled={disableBack || false}
|
}}
|
||||||
>
|
>
|
||||||
Back
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{optional && (
|
|
||||||
<Button
|
|
||||||
variant='tertiary'
|
|
||||||
onClick={() => {
|
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
|
||||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
|
||||||
module: AMPLITUDE_MODULE_NAME,
|
|
||||||
button_id: reviewAndFinishBtnID,
|
|
||||||
active_step_id: activeStep.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!beforeNext || beforeNext()) goToStepById('step-review');
|
|
||||||
}}
|
|
||||||
isDisabled={disableNext}
|
|
||||||
>
|
|
||||||
Review and finish
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant='link'
|
|
||||||
onClick={() => {
|
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
|
||||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
|
||||||
module: AMPLITUDE_MODULE_NAME,
|
|
||||||
button_id: cancelBtnID,
|
|
||||||
active_step_id: activeStep.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</WizardFooterWrapper>
|
</WizardFooterWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -196,19 +193,23 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
const { isFedoraEnv } = useGetEnvironment();
|
||||||
// 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');
|
const isUsersEnabled = useFlag('image-builder.users.enabled');
|
||||||
|
|
||||||
// IMPORTANT: Ensure the wizard starts with a fresh initial state
|
// IMPORTANT: Ensure the wizard starts with a fresh initial state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(initializeWizard());
|
dispatch(initializeWizard());
|
||||||
|
if (isFedoraEnv) {
|
||||||
|
dispatch(changeDistribution(CENTOS_9));
|
||||||
|
dispatch(changeRegistrationType('register-later'));
|
||||||
|
}
|
||||||
if (searchParams.get('release') === 'rhel8') {
|
if (searchParams.get('release') === 'rhel8') {
|
||||||
dispatch(changeDistribution(RHEL_8));
|
dispatch(changeDistribution(RHEL_8));
|
||||||
}
|
}
|
||||||
if (searchParams.get('release') === 'rhel9') {
|
if (searchParams.get('release') === 'rhel10beta') {
|
||||||
dispatch(changeDistribution(RHEL_9));
|
dispatch(changeDistribution(RHEL_10_BETA));
|
||||||
}
|
}
|
||||||
if (searchParams.get('release') === 'rhel10') {
|
if (searchParams.get('release') === 'rhel10') {
|
||||||
dispatch(changeDistribution(RHEL_10));
|
dispatch(changeDistribution(RHEL_10));
|
||||||
|
|
@ -233,10 +234,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
dispatch(changeArchitecture(arch));
|
dispatch(changeArchitecture(arch));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.IS_ON_PREMISE) {
|
|
||||||
dispatch(changeAwsShareMethod('manual'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.IS_ON_PREMISE && !isEdit) {
|
if (process.env.IS_ON_PREMISE && !isEdit) {
|
||||||
if (!searchParams.get('release')) {
|
if (!searchParams.get('release')) {
|
||||||
initializeHostDistro();
|
initializeHostDistro();
|
||||||
|
|
@ -264,9 +261,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
const gcpShareMethod = useAppSelector(selectGcpShareMethod);
|
const gcpShareMethod = useAppSelector(selectGcpShareMethod);
|
||||||
const gcpEmail = useAppSelector(selectGcpEmail);
|
const gcpEmail = useAppSelector(selectGcpEmail);
|
||||||
// AZURE
|
// AZURE
|
||||||
|
const azureShareMethod = useAppSelector(selectAzureShareMethod);
|
||||||
const azureTenantId = useAppSelector(selectAzureTenantId);
|
const azureTenantId = useAppSelector(selectAzureTenantId);
|
||||||
const azureSubscriptionId = useAppSelector(selectAzureSubscriptionId);
|
const azureSubscriptionId = useAppSelector(selectAzureSubscriptionId);
|
||||||
const azureResourceGroup = useAppSelector(selectAzureResourceGroup);
|
const azureResourceGroup = useAppSelector(selectAzureResourceGroup);
|
||||||
|
const azureSource = useAppSelector(selectAzureSource);
|
||||||
// Registration
|
// Registration
|
||||||
const registrationValidation = useRegistrationValidation();
|
const registrationValidation = useRegistrationValidation();
|
||||||
// Snapshots
|
// Snapshots
|
||||||
|
|
@ -286,8 +285,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
const firewallValidation = useFirewallValidation();
|
const firewallValidation = useFirewallValidation();
|
||||||
// Services
|
// Services
|
||||||
const servicesValidation = useServicesValidation();
|
const servicesValidation = useServicesValidation();
|
||||||
// AAP
|
|
||||||
const aapValidation = useAAPValidation();
|
|
||||||
// Firstboot
|
// Firstboot
|
||||||
const firstBootValidation = useFirstBootValidation();
|
const firstBootValidation = useFirstBootValidation();
|
||||||
// Details
|
// Details
|
||||||
|
|
@ -295,51 +292,45 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
// Users
|
// Users
|
||||||
const usersValidation = useUsersValidation();
|
const usersValidation = useUsersValidation();
|
||||||
|
|
||||||
const hasWslTargetOnly = useHasSpecificTargetOnly('wsl');
|
|
||||||
|
|
||||||
let startIndex = 1; // default index
|
let startIndex = 1; // default index
|
||||||
const JUMP_TO_REVIEW_STEP = 23;
|
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
startIndex = JUMP_TO_REVIEW_STEP;
|
startIndex = 22;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
|
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
|
||||||
|
|
||||||
// Duplicating some of the logic from the Wizard component to allow for custom nav items status
|
// Duplicating some of the logic from the Wizard component to allow for custom nav items status
|
||||||
// for original code see https://github.com/patternfly/patternfly-react/blob/184c55f8d10e1d94ffd72e09212db56c15387c5e/packages/react-core/src/components/Wizard/WizardNavInternal.tsx#L128
|
// for original code see https://github.com/patternfly/patternfly-react/blob/184c55f8d10e1d94ffd72e09212db56c15387c5e/packages/react-core/src/components/Wizard/WizardNavInternal.tsx#L128
|
||||||
const CustomStatusNavItem = (
|
const customStatusNavItem = (
|
||||||
step: WizardStepType,
|
step: WizardStepType,
|
||||||
activeStep: WizardStepType,
|
activeStep: WizardStepType,
|
||||||
steps: WizardStepType[],
|
steps: WizardStepType[],
|
||||||
goToStepByIndex: (index: number) => void,
|
goToStepByIndex: (index: number) => void
|
||||||
) => {
|
) => {
|
||||||
const isVisitOptional =
|
const isVisitOptional =
|
||||||
'parentId' in step && step.parentId === 'step-optional-steps';
|
'parentId' in step && step.parentId === 'step-optional-steps';
|
||||||
|
|
||||||
useEffect(() => {
|
if (process.env.IS_ON_PREMISE) {
|
||||||
if (process.env.IS_ON_PREMISE) {
|
if (step.id === 'step-oscap' && step.isVisited) {
|
||||||
if (step.id === 'step-oscap' && step.isVisited) {
|
|
||||||
setWasRegisterVisited(true);
|
|
||||||
}
|
|
||||||
} else if (step.id === 'step-register' && step.isVisited) {
|
|
||||||
setWasRegisterVisited(true);
|
setWasRegisterVisited(true);
|
||||||
}
|
}
|
||||||
}, [step.id, step.isVisited]);
|
} else if (step.id === 'step-register' && step.isVisited) {
|
||||||
|
setWasRegisterVisited(true);
|
||||||
|
}
|
||||||
|
|
||||||
const hasVisitedNextStep = steps.some(
|
const hasVisitedNextStep = steps.some(
|
||||||
(s) => s.index > step.index && s.isVisited,
|
(s) => s.index > step.index && s.isVisited
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only this code is different from the original
|
// Only this code is different from the original
|
||||||
const status = (step.id !== activeStep.id && step.status) || 'default';
|
const status = (step?.id !== activeStep?.id && step?.status) || 'default';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WizardNavItem
|
<WizardNavItem
|
||||||
key={step.id}
|
key={step?.id}
|
||||||
id={step.id}
|
id={step?.id}
|
||||||
content={step.name}
|
content={step.name}
|
||||||
isCurrent={activeStep.id === step.id}
|
isCurrent={activeStep?.id === step?.id}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
step.isDisabled ||
|
step.isDisabled ||
|
||||||
(!step.isVisited &&
|
(!step.isVisited &&
|
||||||
|
|
@ -356,7 +347,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
{
|
{
|
||||||
module: AMPLITUDE_MODULE_NAME,
|
module: AMPLITUDE_MODULE_NAME,
|
||||||
isPreview: isBeta(),
|
isPreview: isBeta(),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
@ -368,15 +359,15 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ImageBuilderHeader inWizard />
|
<ImageBuilderHeader inWizard />
|
||||||
<PageSection hasBodyWrapper={false} type={PageSectionTypes.wizard}>
|
<PageSection type={PageSectionTypes.wizard}>
|
||||||
<Wizard
|
<Wizard
|
||||||
startIndex={startIndex}
|
startIndex={startIndex}
|
||||||
onClose={() => navigate(resolveRelPath(''))}
|
onClose={() => navigate(resolveRelPath(''))}
|
||||||
isVisitRequired
|
isVisitRequired
|
||||||
>
|
>
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Image output'
|
name="Image output"
|
||||||
id='step-image-output'
|
id="step-image-output"
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
disableNext={targetEnvironments.length === 0}
|
disableNext={targetEnvironments.length === 0}
|
||||||
|
|
@ -387,29 +378,25 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<ImageOutputStep />
|
<ImageOutputStep />
|
||||||
</WizardStep>
|
</WizardStep>
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Target Environment'
|
name="Target Environment"
|
||||||
id='step-target-environment'
|
id="step-target-environment"
|
||||||
isHidden={
|
isHidden={
|
||||||
!targetEnvironments.find(
|
!targetEnvironments.find(
|
||||||
(target: string) =>
|
(target) =>
|
||||||
target === 'aws' || target === 'gcp' || target === 'azure',
|
target === 'aws' || target === 'gcp' || target === 'azure'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
steps={[
|
steps={[
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Amazon Web Services'
|
name="Amazon Web Services"
|
||||||
id='wizard-target-aws'
|
id="wizard-target-aws"
|
||||||
key='wizard-target-aws'
|
key="wizard-target-aws"
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
disableNext={
|
disableNext={
|
||||||
// we don't need the account id for
|
awsShareMethod === 'manual'
|
||||||
// on-prem aws.
|
? !isAwsAccountIdValid(awsAccountId)
|
||||||
process.env.IS_ON_PREMISE
|
: awsSourceId === undefined
|
||||||
? false
|
|
||||||
: awsShareMethod === 'manual'
|
|
||||||
? !isAwsAccountIdValid(awsAccountId)
|
|
||||||
: awsSourceId === undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
@ -418,9 +405,9 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<Aws />
|
<Aws />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Google Cloud Platform'
|
name="Google Cloud Platform"
|
||||||
id='wizard-target-gcp'
|
id="wizard-target-gcp"
|
||||||
key='wizard-target-gcp'
|
key="wizard-target-gcp"
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
disableNext={
|
disableNext={
|
||||||
|
|
@ -434,15 +421,21 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<Gcp />
|
<Gcp />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Azure'
|
name="Azure"
|
||||||
id='wizard-target-azure'
|
id="wizard-target-azure"
|
||||||
key='wizard-target-azure'
|
key="wizard-target-azure"
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
disableNext={
|
disableNext={
|
||||||
!isAzureTenantGUIDValid(azureTenantId) ||
|
azureShareMethod === 'manual'
|
||||||
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
|
? !isAzureTenantGUIDValid(azureTenantId) ||
|
||||||
!isAzureResourceGroupValid(azureResourceGroup)
|
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
|
||||||
|
!isAzureResourceGroupValid(azureResourceGroup)
|
||||||
|
: azureShareMethod === 'sources'
|
||||||
|
? !isAzureTenantGUIDValid(azureTenantId) ||
|
||||||
|
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
|
||||||
|
!isAzureResourceGroupValid(azureResourceGroup)
|
||||||
|
: azureSource === undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
@ -453,15 +446,15 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Optional steps'
|
name="Optional steps"
|
||||||
id='step-optional-steps'
|
id="step-optional-steps"
|
||||||
steps={[
|
steps={[
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Register'
|
name="Register"
|
||||||
id='step-register'
|
id="step-register"
|
||||||
key='step-register'
|
key="step-register"
|
||||||
isHidden={!!process.env.IS_ON_PREMISE || !isRhel(distribution)}
|
isHidden={!!process.env.IS_ON_PREMISE || !isRhel(distribution)}
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
status={
|
status={
|
||||||
wasRegisterVisited
|
wasRegisterVisited
|
||||||
? registrationValidation.disabledNext
|
? registrationValidation.disabledNext
|
||||||
|
|
@ -480,9 +473,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name={complianceEnabled ? 'Compliance' : 'OpenSCAP'}
|
name={complianceEnabled ? 'Compliance' : 'OpenSCAP'}
|
||||||
id='step-oscap'
|
id="step-oscap"
|
||||||
key='step-oscap'
|
key="step-oscap"
|
||||||
navItem={CustomStatusNavItem}
|
isHidden={distribution === RHEL_10_BETA}
|
||||||
|
navItem={customStatusNavItem}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter disableNext={false} optional={true} />
|
<CustomWizardFooter disableNext={false} optional={true} />
|
||||||
}
|
}
|
||||||
|
|
@ -490,11 +484,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<OscapStep />
|
<OscapStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='File system configuration'
|
name="File system configuration"
|
||||||
id='step-file-system'
|
id="step-file-system"
|
||||||
key='step-file-system'
|
key="step-file-system"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
isHidden={hasWslTargetOnly}
|
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
beforeNext={() => {
|
beforeNext={() => {
|
||||||
|
|
@ -516,12 +509,16 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
</FileSystemContext.Provider>
|
</FileSystemContext.Provider>
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Repeatable build'
|
name="Repeatable build"
|
||||||
id='wizard-repository-snapshot'
|
id="wizard-repository-snapshot"
|
||||||
key='wizard-repository-snapshot'
|
key="wizard-repository-snapshot"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
status={snapshotValidation.disabledNext ? 'error' : 'default'}
|
status={snapshotValidation.disabledNext ? 'error' : 'default'}
|
||||||
isHidden={!!process.env.IS_ON_PREMISE}
|
isHidden={
|
||||||
|
distribution === RHEL_10_BETA ||
|
||||||
|
!!process.env.IS_ON_PREMISE ||
|
||||||
|
isFedoraEnv
|
||||||
|
}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
disableNext={snapshotValidation.disabledNext}
|
disableNext={snapshotValidation.disabledNext}
|
||||||
|
|
@ -532,11 +529,15 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<SnapshotStep />
|
<SnapshotStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Custom repositories'
|
name="Custom repositories"
|
||||||
id='wizard-custom-repositories'
|
id="wizard-custom-repositories"
|
||||||
key='wizard-custom-repositories'
|
key="wizard-custom-repositories"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
isHidden={!!process.env.IS_ON_PREMISE}
|
isHidden={
|
||||||
|
distribution === RHEL_10_BETA ||
|
||||||
|
!!process.env.IS_ON_PREMISE ||
|
||||||
|
isFedoraEnv
|
||||||
|
}
|
||||||
isDisabled={snapshotValidation.disabledNext}
|
isDisabled={snapshotValidation.disabledNext}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter disableNext={false} optional={true} />
|
<CustomWizardFooter disableNext={false} optional={true} />
|
||||||
|
|
@ -545,10 +546,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<RepositoriesStep />
|
<RepositoriesStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Additional packages'
|
name="Additional packages"
|
||||||
id='wizard-additional-packages'
|
id="wizard-additional-packages"
|
||||||
key='wizard-additional-packages'
|
key="wizard-additional-packages"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
|
isHidden={isFedoraEnv}
|
||||||
isDisabled={snapshotValidation.disabledNext}
|
isDisabled={snapshotValidation.disabledNext}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter disableNext={false} optional={true} />
|
<CustomWizardFooter disableNext={false} optional={true} />
|
||||||
|
|
@ -557,10 +559,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<PackagesStep />
|
<PackagesStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Users'
|
name="Users"
|
||||||
id='wizard-users'
|
id="wizard-users"
|
||||||
key='wizard-users'
|
key="wizard-users"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
|
isHidden={!isUsersEnabled}
|
||||||
status={usersValidation.disabledNext ? 'error' : 'default'}
|
status={usersValidation.disabledNext ? 'error' : 'default'}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
|
|
@ -572,10 +575,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<UsersStep />
|
<UsersStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Timezone'
|
name="Timezone"
|
||||||
id='wizard-timezone'
|
id="wizard-timezone"
|
||||||
key='wizard-timezone'
|
key="wizard-timezone"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
status={timezoneValidation.disabledNext ? 'error' : 'default'}
|
status={timezoneValidation.disabledNext ? 'error' : 'default'}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
|
|
@ -587,10 +590,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<TimezoneStep />
|
<TimezoneStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Locale'
|
name="Locale"
|
||||||
id='wizard-locale'
|
id="wizard-locale"
|
||||||
key='wizard-locale'
|
key="wizard-locale"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
status={localeValidation.disabledNext ? 'error' : 'default'}
|
status={localeValidation.disabledNext ? 'error' : 'default'}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
|
|
@ -602,10 +605,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<LocaleStep />
|
<LocaleStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Hostname'
|
name="Hostname"
|
||||||
id='wizard-hostname'
|
id="wizard-hostname"
|
||||||
key='wizard-hostname'
|
key="wizard-hostname"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
status={hostnameValidation.disabledNext ? 'error' : 'default'}
|
status={hostnameValidation.disabledNext ? 'error' : 'default'}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
|
|
@ -617,11 +620,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<HostnameStep />
|
<HostnameStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Kernel'
|
name="Kernel"
|
||||||
id='wizard-kernel'
|
id="wizard-kernel"
|
||||||
key='wizard-kernel'
|
key="wizard-kernel"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
isHidden={hasWslTargetOnly}
|
|
||||||
status={kernelValidation.disabledNext ? 'error' : 'default'}
|
status={kernelValidation.disabledNext ? 'error' : 'default'}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
|
|
@ -633,10 +635,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<KernelStep />
|
<KernelStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Firewall'
|
name="Firewall"
|
||||||
id='wizard-firewall'
|
id="wizard-firewall"
|
||||||
key='wizard-firewall'
|
key="wizard-firewall"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
status={firewallValidation.disabledNext ? 'error' : 'default'}
|
status={firewallValidation.disabledNext ? 'error' : 'default'}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
|
|
@ -648,10 +650,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<FirewallStep />
|
<FirewallStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Systemd services'
|
name="Systemd services"
|
||||||
id='wizard-services'
|
id="wizard-services"
|
||||||
key='wizard-services'
|
key="wizard-services"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
status={servicesValidation.disabledNext ? 'error' : 'default'}
|
status={servicesValidation.disabledNext ? 'error' : 'default'}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
|
|
@ -663,28 +665,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<ServicesStep />
|
<ServicesStep />
|
||||||
</WizardStep>,
|
</WizardStep>,
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Ansible Automation Platform'
|
name="First boot script configuration"
|
||||||
id='wizard-aap'
|
id="wizard-first-boot"
|
||||||
isHidden={!isAAPRegistrationEnabled}
|
key="wizard-first-boot"
|
||||||
key='wizard-aap'
|
navItem={customStatusNavItem}
|
||||||
navItem={CustomStatusNavItem}
|
|
||||||
status={aapValidation.disabledNext ? 'error' : 'default'}
|
|
||||||
footer={
|
|
||||||
<CustomWizardFooter
|
|
||||||
disableNext={aapValidation.disabledNext}
|
|
||||||
optional={true}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AAPStep />
|
|
||||||
</WizardStep>,
|
|
||||||
<WizardStep
|
|
||||||
name='First boot script configuration'
|
|
||||||
id='wizard-first-boot'
|
|
||||||
key='wizard-first-boot'
|
|
||||||
navItem={CustomStatusNavItem}
|
|
||||||
status={firstBootValidation.disabledNext ? 'error' : 'default'}
|
status={firstBootValidation.disabledNext ? 'error' : 'default'}
|
||||||
isHidden={!!process.env.IS_ON_PREMISE}
|
isHidden={!!process.env.IS_ON_PREMISE || isFedoraEnv}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
disableNext={firstBootValidation.disabledNext}
|
disableNext={firstBootValidation.disabledNext}
|
||||||
|
|
@ -697,9 +683,9 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Details'
|
name="Details"
|
||||||
id='step-details'
|
id="step-details"
|
||||||
navItem={CustomStatusNavItem}
|
navItem={customStatusNavItem}
|
||||||
status={detailsValidation.disabledNext ? 'error' : 'default'}
|
status={detailsValidation.disabledNext ? 'error' : 'default'}
|
||||||
footer={
|
footer={
|
||||||
<CustomWizardFooter
|
<CustomWizardFooter
|
||||||
|
|
@ -710,8 +696,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
||||||
<DetailsStep />
|
<DetailsStep />
|
||||||
</WizardStep>
|
</WizardStep>
|
||||||
<WizardStep
|
<WizardStep
|
||||||
name='Review'
|
name="Review"
|
||||||
id='step-review'
|
id="step-review"
|
||||||
footer={<ReviewWizardFooter />}
|
footer={<ReviewWizardFooter />}
|
||||||
>
|
>
|
||||||
<ReviewStep />
|
<ReviewStep />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { useAddNotification } from '@redhat-cloud-services/frontend-components-notifications/hooks';
|
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import CreateImageWizard from './CreateImageWizard';
|
import CreateImageWizard from './CreateImageWizard';
|
||||||
|
|
@ -13,18 +13,19 @@ const ImportImageWizard = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const addNotification = useAddNotification();
|
|
||||||
const locationState = location.state as { blueprint?: wizardState };
|
const locationState = location.state as { blueprint?: wizardState };
|
||||||
const blueprint = locationState.blueprint;
|
const blueprint = locationState?.blueprint;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (blueprint) {
|
if (blueprint) {
|
||||||
dispatch(loadWizardState(blueprint));
|
dispatch(loadWizardState(blueprint));
|
||||||
} else {
|
} else {
|
||||||
navigate(resolveRelPath(''));
|
navigate(resolveRelPath(''));
|
||||||
addNotification({
|
dispatch(
|
||||||
variant: 'warning',
|
addNotification({
|
||||||
title: 'No blueprint was imported',
|
variant: 'warning',
|
||||||
});
|
title: 'No blueprint was imported',
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [blueprint, dispatch]);
|
}, [blueprint, dispatch]);
|
||||||
return <CreateImageWizard />;
|
return <CreateImageWizard />;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import {
|
||||||
Button,
|
Button,
|
||||||
HelperText,
|
HelperText,
|
||||||
HelperTextItem,
|
HelperTextItem,
|
||||||
Icon,
|
|
||||||
Label,
|
Label,
|
||||||
LabelGroup,
|
LabelGroup,
|
||||||
TextInputGroup,
|
TextInputGroup,
|
||||||
|
|
@ -48,73 +47,30 @@ const LabelInput = ({
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [onStepInputErrorText, setOnStepInputErrorText] = useState('');
|
const [errorText, setErrorText] = useState(stepValidation.errors[fieldName]);
|
||||||
let [invalidImports, duplicateImports] = ['', ''];
|
|
||||||
|
|
||||||
if (stepValidation.errors[fieldName]) {
|
|
||||||
[invalidImports, duplicateImports] =
|
|
||||||
stepValidation.errors[fieldName].split('|');
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTextInputChange = (
|
const onTextInputChange = (
|
||||||
_event: React.FormEvent<HTMLInputElement>,
|
_event: React.FormEvent<HTMLInputElement>,
|
||||||
value: string,
|
value: string
|
||||||
) => {
|
) => {
|
||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
setOnStepInputErrorText('');
|
setErrorText('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const addItem = (value: string) => {
|
const addItem = (value: string) => {
|
||||||
if (list?.includes(value) || requiredList?.includes(value)) {
|
if (list?.includes(value) || requiredList?.includes(value)) {
|
||||||
setOnStepInputErrorText(`${item} already exists.`);
|
setErrorText(`${item} already exists.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validator(value)) {
|
if (!validator(value)) {
|
||||||
switch (fieldName) {
|
setErrorText('Invalid format.');
|
||||||
case 'ports':
|
|
||||||
setOnStepInputErrorText(
|
|
||||||
'Expected format: <port/port-name>:<protocol>. Example: 8080:tcp, ssh:tcp',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'kernelAppend':
|
|
||||||
setOnStepInputErrorText(
|
|
||||||
'Expected format: <kernel-argument>. Example: console=tty0',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'kernelName':
|
|
||||||
setOnStepInputErrorText(
|
|
||||||
'Expected format: <kernel-name>. Example: kernel-5.14.0-284.11.1.el9_2.x86_64',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'groups':
|
|
||||||
setOnStepInputErrorText(
|
|
||||||
'Expected format: <group-name>. Example: admin',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'ntpServers':
|
|
||||||
setOnStepInputErrorText(
|
|
||||||
'Expected format: <ntp-server>. Example: time.redhat.com',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'enabledSystemdServices':
|
|
||||||
case 'disabledSystemdServices':
|
|
||||||
case 'maskedSystemdServices':
|
|
||||||
case 'disabledServices':
|
|
||||||
case 'enabledServices':
|
|
||||||
setOnStepInputErrorText(
|
|
||||||
'Expected format: <service-name>. Example: sshd',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setOnStepInputErrorText('Invalid format.');
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(addAction(value));
|
dispatch(addAction(value));
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
setOnStepInputErrorText('');
|
setErrorText('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent, value: string) => {
|
const handleKeyDown = (e: React.KeyboardEvent, value: string) => {
|
||||||
|
|
@ -130,18 +86,14 @@ const LabelInput = ({
|
||||||
|
|
||||||
const handleRemoveItem = (e: React.MouseEvent, value: string) => {
|
const handleRemoveItem = (e: React.MouseEvent, value: string) => {
|
||||||
dispatch(removeAction(value));
|
dispatch(removeAction(value));
|
||||||
|
setErrorText('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
setOnStepInputErrorText('');
|
setErrorText('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
if (onStepInputErrorText) errors.push(onStepInputErrorText);
|
|
||||||
if (invalidImports) errors.push(invalidImports);
|
|
||||||
if (duplicateImports) errors.push(duplicateImports);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextInputGroup>
|
<TextInputGroup>
|
||||||
|
|
@ -153,39 +105,33 @@ const LabelInput = ({
|
||||||
/>
|
/>
|
||||||
<TextInputGroupUtilities>
|
<TextInputGroupUtilities>
|
||||||
<Button
|
<Button
|
||||||
icon={
|
variant="plain"
|
||||||
<Icon status='info'>
|
|
||||||
<PlusCircleIcon />
|
|
||||||
</Icon>
|
|
||||||
}
|
|
||||||
variant='plain'
|
|
||||||
onClick={(e) => handleAddItem(e, inputValue)}
|
onClick={(e) => handleAddItem(e, inputValue)}
|
||||||
isDisabled={!inputValue}
|
isDisabled={!inputValue}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
/>
|
>
|
||||||
|
<PlusCircleIcon className="pf-v5-u-primary-color-100" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
icon={<TimesIcon />}
|
variant="plain"
|
||||||
variant='plain'
|
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
isDisabled={!inputValue}
|
isDisabled={!inputValue}
|
||||||
aria-label='Clear input'
|
aria-label="Clear input"
|
||||||
/>
|
>
|
||||||
|
<TimesIcon />
|
||||||
|
</Button>
|
||||||
</TextInputGroupUtilities>
|
</TextInputGroupUtilities>
|
||||||
</TextInputGroup>
|
</TextInputGroup>
|
||||||
{errors.length > 0 && (
|
{errorText && (
|
||||||
<HelperText>
|
<HelperText>
|
||||||
{errors.map((error, index) => (
|
<HelperTextItem variant={'error'}>{errorText}</HelperTextItem>
|
||||||
<HelperTextItem key={index} variant={'error'}>
|
|
||||||
{error}
|
|
||||||
</HelperTextItem>
|
|
||||||
))}
|
|
||||||
</HelperText>
|
</HelperText>
|
||||||
)}
|
)}
|
||||||
{requiredList && requiredList.length > 0 && (
|
{requiredList && requiredList.length > 0 && (
|
||||||
<LabelGroup
|
<LabelGroup
|
||||||
categoryName={requiredCategoryName}
|
categoryName={requiredCategoryName}
|
||||||
numLabels={20}
|
numLabels={20}
|
||||||
className='pf-v6-u-mt-sm pf-v6-u-w-100'
|
className="pf-v5-u-mt-sm pf-v5-u-w-100"
|
||||||
>
|
>
|
||||||
{requiredList.map((item) => (
|
{requiredList.map((item) => (
|
||||||
<Label key={item} isCompact>
|
<Label key={item} isCompact>
|
||||||
|
|
@ -194,7 +140,7 @@ const LabelInput = ({
|
||||||
))}
|
))}
|
||||||
</LabelGroup>
|
</LabelGroup>
|
||||||
)}
|
)}
|
||||||
<LabelGroup numLabels={20} className='pf-v6-u-mt-sm pf-v6-u-w-100'>
|
<LabelGroup numLabels={20} className="pf-v5-u-mt-sm pf-v5-u-w-100">
|
||||||
{list?.map((item) => (
|
{list?.map((item) => (
|
||||||
<Label
|
<Label
|
||||||
key={item}
|
key={item}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { Alert } from '@patternfly/react-core';
|
||||||
const UsrSubDirectoriesDisabled = () => {
|
const UsrSubDirectoriesDisabled = () => {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
variant='warning'
|
variant="warning"
|
||||||
title='Sub-directories for the /usr mount point are no longer supported'
|
title="Sub-directories for the /usr mount point are no longer supported"
|
||||||
isInline
|
isInline
|
||||||
>
|
>
|
||||||
Please note that including sub-directories in the /usr path is no longer
|
Please note that including sub-directories in the /usr path is no longer
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -31,7 +31,7 @@ type ValidationInputProp = TextInputProps &
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
onChange: (
|
onChange: (
|
||||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
value: string,
|
value: string
|
||||||
) => void;
|
) => void;
|
||||||
isRequired?: boolean;
|
isRequired?: boolean;
|
||||||
warning?: string;
|
warning?: string;
|
||||||
|
|
@ -91,14 +91,16 @@ 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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{warning !== undefined && warning !== '' && (
|
{warning !== undefined && warning !== '' && (
|
||||||
<HelperText>
|
<HelperText>
|
||||||
<HelperTextItem variant='warning'>{warning}</HelperTextItem>
|
<HelperTextItem variant="warning" hasIcon>
|
||||||
|
{warning}
|
||||||
|
</HelperTextItem>
|
||||||
</HelperText>
|
</HelperText>
|
||||||
)}
|
)}
|
||||||
{validated === 'error' && hasError && (
|
{validated === 'error' && hasError && (
|
||||||
|
|
@ -111,13 +113,13 @@ export const ValidatedInputAndTextArea = ({
|
||||||
const getValidationState = (
|
const getValidationState = (
|
||||||
isPristine: boolean,
|
isPristine: boolean,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
isRequired: boolean | undefined,
|
isRequired: boolean | undefined
|
||||||
): ValidationResult => {
|
): ValidationResult => {
|
||||||
const validated = isPristine
|
const validated = isPristine
|
||||||
? 'default'
|
? 'default'
|
||||||
: (isRequired && errorMessage) || errorMessage
|
: (isRequired && errorMessage) || errorMessage
|
||||||
? 'error'
|
? 'error'
|
||||||
: 'success';
|
: 'success';
|
||||||
|
|
||||||
return validated;
|
return validated;
|
||||||
};
|
};
|
||||||
|
|
@ -125,7 +127,9 @@ const getValidationState = (
|
||||||
export const ErrorMessage = ({ errorMessage }: ErrorMessageProps) => {
|
export const ErrorMessage = ({ errorMessage }: ErrorMessageProps) => {
|
||||||
return (
|
return (
|
||||||
<HelperText>
|
<HelperText>
|
||||||
<HelperTextItem variant='error'>{errorMessage}</HelperTextItem>
|
<HelperTextItem variant="error" hasIcon>
|
||||||
|
{errorMessage}
|
||||||
|
</HelperTextItem>
|
||||||
</HelperText>
|
</HelperText>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -138,7 +142,6 @@ export const ValidatedInput = ({
|
||||||
value,
|
value,
|
||||||
placeholder,
|
placeholder,
|
||||||
onChange,
|
onChange,
|
||||||
...props
|
|
||||||
}: ValidatedTextInputPropTypes) => {
|
}: ValidatedTextInputPropTypes) => {
|
||||||
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
||||||
|
|
||||||
|
|
@ -159,17 +162,18 @@ export const ValidatedInput = ({
|
||||||
<TextInput
|
<TextInput
|
||||||
value={value}
|
value={value}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
type='text'
|
type="text"
|
||||||
onChange={onChange!}
|
onChange={onChange!}
|
||||||
validated={handleValidation()}
|
validated={handleValidation()}
|
||||||
aria-label={ariaLabel || ''}
|
aria-label={ariaLabel || ''}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
placeholder={placeholder || ''}
|
placeholder={placeholder || ''}
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
{!isPristine && !validator(value) && (
|
{!isPristine && !validator(value) && (
|
||||||
<HelperText>
|
<HelperText>
|
||||||
<HelperTextItem variant='error'>{helperText}</HelperTextItem>
|
<HelperTextItem variant="error" hasIcon>
|
||||||
|
{helperText}
|
||||||
|
</HelperTextItem>
|
||||||
</HelperText>
|
</HelperText>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
DropEvent,
|
|
||||||
FileUpload,
|
|
||||||
FormGroup,
|
|
||||||
FormHelperText,
|
|
||||||
HelperText,
|
|
||||||
HelperTextItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
|
||||||
import {
|
|
||||||
changeAapCallbackUrl,
|
|
||||||
changeAapHostConfigKey,
|
|
||||||
changeAapTlsCertificateAuthority,
|
|
||||||
changeAapTlsConfirmation,
|
|
||||||
selectAapCallbackUrl,
|
|
||||||
selectAapHostConfigKey,
|
|
||||||
selectAapTlsCertificateAuthority,
|
|
||||||
selectAapTlsConfirmation,
|
|
||||||
} from '../../../../../store/wizardSlice';
|
|
||||||
import { useAAPValidation } from '../../../utilities/useValidation';
|
|
||||||
import { ValidatedInputAndTextArea } from '../../../ValidatedInput';
|
|
||||||
import { validateMultipleCertificates } from '../../../validators';
|
|
||||||
|
|
||||||
const AAPRegistration = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const callbackUrl = useAppSelector(selectAapCallbackUrl);
|
|
||||||
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
|
|
||||||
const tlsCertificateAuthority = useAppSelector(
|
|
||||||
selectAapTlsCertificateAuthority,
|
|
||||||
);
|
|
||||||
const tlsConfirmation = useAppSelector(selectAapTlsConfirmation);
|
|
||||||
const [isRejected, setIsRejected] = React.useState(false);
|
|
||||||
const stepValidation = useAAPValidation();
|
|
||||||
|
|
||||||
const isHttpsUrl = callbackUrl?.toLowerCase().startsWith('https://') || false;
|
|
||||||
const shouldShowCaInput = !isHttpsUrl || (isHttpsUrl && !tlsConfirmation);
|
|
||||||
|
|
||||||
const validated = stepValidation.errors['certificate']
|
|
||||||
? 'error'
|
|
||||||
: stepValidation.errors['certificate'] === undefined &&
|
|
||||||
tlsCertificateAuthority &&
|
|
||||||
validateMultipleCertificates(tlsCertificateAuthority).validCertificates
|
|
||||||
.length > 0
|
|
||||||
? 'success'
|
|
||||||
: 'default';
|
|
||||||
|
|
||||||
const handleCallbackUrlChange = (value: string) => {
|
|
||||||
dispatch(changeAapCallbackUrl(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHostConfigKeyChange = (value: string) => {
|
|
||||||
dispatch(changeAapHostConfigKey(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClear = () => {
|
|
||||||
dispatch(changeAapTlsCertificateAuthority(''));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTextChange = (
|
|
||||||
_event: React.ChangeEvent<HTMLTextAreaElement>,
|
|
||||||
value: string,
|
|
||||||
) => {
|
|
||||||
dispatch(changeAapTlsCertificateAuthority(value));
|
|
||||||
setIsRejected(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDataChange = (_: DropEvent, value: string) => {
|
|
||||||
dispatch(changeAapTlsCertificateAuthority(value));
|
|
||||||
setIsRejected(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileRejected = () => {
|
|
||||||
dispatch(changeAapTlsCertificateAuthority(''));
|
|
||||||
setIsRejected(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTlsConfirmationChange = (checked: boolean) => {
|
|
||||||
dispatch(changeAapTlsConfirmation(checked));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FormGroup label='Ansible Callback URL' isRequired>
|
|
||||||
<ValidatedInputAndTextArea
|
|
||||||
value={callbackUrl || ''}
|
|
||||||
onChange={(_event, value) => handleCallbackUrlChange(value.trim())}
|
|
||||||
ariaLabel='ansible callback url'
|
|
||||||
isRequired
|
|
||||||
stepValidation={stepValidation}
|
|
||||||
fieldName='callbackUrl'
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup label='Host Config Key' isRequired>
|
|
||||||
<ValidatedInputAndTextArea
|
|
||||||
value={hostConfigKey || ''}
|
|
||||||
onChange={(_event, value) => handleHostConfigKeyChange(value.trim())}
|
|
||||||
ariaLabel='host config key'
|
|
||||||
isRequired
|
|
||||||
stepValidation={stepValidation}
|
|
||||||
fieldName='hostConfigKey'
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{shouldShowCaInput && (
|
|
||||||
<FormGroup label='Certificate authority (CA) for Ansible Controller'>
|
|
||||||
<FileUpload
|
|
||||||
id='aap-certificate-upload'
|
|
||||||
type='text'
|
|
||||||
value={tlsCertificateAuthority || ''}
|
|
||||||
filename={tlsCertificateAuthority ? 'CA detected' : ''}
|
|
||||||
onDataChange={handleDataChange}
|
|
||||||
onTextChange={handleTextChange}
|
|
||||||
onClearClick={handleClear}
|
|
||||||
dropzoneProps={{
|
|
||||||
accept: {
|
|
||||||
'application/x-pem-file': ['.pem'],
|
|
||||||
'application/x-x509-ca-cert': ['.cer', '.crt'],
|
|
||||||
'application/pkix-cert': ['.der'],
|
|
||||||
},
|
|
||||||
maxSize: 512000,
|
|
||||||
onDropRejected: handleFileRejected,
|
|
||||||
}}
|
|
||||||
validated={isRejected ? 'error' : validated}
|
|
||||||
browseButtonText='Upload'
|
|
||||||
allowEditingUploadedText={true}
|
|
||||||
/>
|
|
||||||
<FormHelperText>
|
|
||||||
<HelperText>
|
|
||||||
<HelperTextItem
|
|
||||||
variant={
|
|
||||||
isRejected || validated === 'error'
|
|
||||||
? 'error'
|
|
||||||
: validated === 'success'
|
|
||||||
? 'success'
|
|
||||||
: 'default'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isRejected
|
|
||||||
? 'Must be a .PEM/.CER/.CRT file'
|
|
||||||
: validated === 'error'
|
|
||||||
? stepValidation.errors['certificate']
|
|
||||||
: validated === 'success'
|
|
||||||
? 'Certificate was uploaded'
|
|
||||||
: 'Drag and drop a valid certificate file or upload one'}
|
|
||||||
</HelperTextItem>
|
|
||||||
</HelperText>
|
|
||||||
</FormHelperText>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{isHttpsUrl && (
|
|
||||||
<FormGroup>
|
|
||||||
<Checkbox
|
|
||||||
id='tls-confirmation-checkbox'
|
|
||||||
label='Insecure'
|
|
||||||
isChecked={tlsConfirmation || false}
|
|
||||||
onChange={(_event, checked) => handleTlsConfirmationChange(checked)}
|
|
||||||
/>
|
|
||||||
{stepValidation.errors['tlsConfirmation'] && (
|
|
||||||
<FormHelperText>
|
|
||||||
<HelperText>
|
|
||||||
<HelperTextItem variant='error'>
|
|
||||||
{stepValidation.errors['tlsConfirmation']}
|
|
||||||
</HelperTextItem>
|
|
||||||
</HelperText>
|
|
||||||
</FormHelperText>
|
|
||||||
)}
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AAPRegistration;
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Form, Title } from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import AAPRegistration from './components/AAPRegistration';
|
|
||||||
|
|
||||||
const AAPStep = () => {
|
|
||||||
return (
|
|
||||||
<Form>
|
|
||||||
<Title headingLevel='h1' size='xl'>
|
|
||||||
Ansible Automation Platform
|
|
||||||
</Title>
|
|
||||||
<AAPRegistration />
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AAPStep;
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Content,
|
|
||||||
Form,
|
Form,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
FormHelperText,
|
FormHelperText,
|
||||||
HelperText,
|
HelperText,
|
||||||
HelperTextItem,
|
HelperTextItem,
|
||||||
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ const DetailsStep = () => {
|
||||||
|
|
||||||
const handleNameChange = (
|
const handleNameChange = (
|
||||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
name: string,
|
name: string
|
||||||
) => {
|
) => {
|
||||||
dispatch(changeBlueprintName(name));
|
dispatch(changeBlueprintName(name));
|
||||||
dispatch(setIsCustomName());
|
dispatch(setIsCustomName());
|
||||||
|
|
@ -36,7 +36,7 @@ const DetailsStep = () => {
|
||||||
|
|
||||||
const handleDescriptionChange = (
|
const handleDescriptionChange = (
|
||||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
description: string,
|
description: string
|
||||||
) => {
|
) => {
|
||||||
dispatch(changeBlueprintDescription(description));
|
dispatch(changeBlueprintDescription(description));
|
||||||
};
|
};
|
||||||
|
|
@ -45,23 +45,23 @@ const DetailsStep = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<Title headingLevel='h1' size='xl'>
|
<Title headingLevel="h1" size="xl">
|
||||||
Details
|
Details
|
||||||
</Title>
|
</Title>
|
||||||
<Content>
|
<Text>
|
||||||
Enter a name to identify your blueprint. If no name is entered, the
|
Enter a name to identify your blueprint. If no name is entered, the
|
||||||
images created from this blueprint will use the name of the parent
|
images created from this blueprint will use the name of the parent
|
||||||
blueprint.
|
blueprint.
|
||||||
</Content>
|
</Text>
|
||||||
<FormGroup isRequired label='Blueprint name' fieldId='blueprint-name'>
|
<FormGroup isRequired label="Blueprint name" fieldId="blueprint-name">
|
||||||
<ValidatedInputAndTextArea
|
<ValidatedInputAndTextArea
|
||||||
ariaLabel='blueprint name'
|
ariaLabel="blueprint name"
|
||||||
dataTestId='blueprint'
|
dataTestId="blueprint"
|
||||||
value={blueprintName}
|
value={blueprintName}
|
||||||
onChange={handleNameChange}
|
onChange={handleNameChange}
|
||||||
placeholder='Add blueprint name'
|
placeholder="Add blueprint name"
|
||||||
stepValidation={stepValidation}
|
stepValidation={stepValidation}
|
||||||
fieldName='name'
|
fieldName="name"
|
||||||
isRequired={true}
|
isRequired={true}
|
||||||
/>
|
/>
|
||||||
<FormHelperText>
|
<FormHelperText>
|
||||||
|
|
@ -75,17 +75,17 @@ const DetailsStep = () => {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label='Blueprint description'
|
label="Blueprint description"
|
||||||
fieldId='blueprint-description-name'
|
fieldId="blueprint-description-name"
|
||||||
>
|
>
|
||||||
<ValidatedInputAndTextArea
|
<ValidatedInputAndTextArea
|
||||||
ariaLabel='blueprint description'
|
ariaLabel="blueprint description"
|
||||||
dataTestId='blueprint description'
|
dataTestId="blueprint description"
|
||||||
value={blueprintDescription}
|
value={blueprintDescription}
|
||||||
onChange={handleDescriptionChange}
|
onChange={handleDescriptionChange}
|
||||||
placeholder='Add description'
|
placeholder="Add description"
|
||||||
stepValidation={stepValidation}
|
stepValidation={stepValidation}
|
||||||
fieldName='description'
|
fieldName="description"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue