Compare commits
No commits in common. "main" and "v69" have entirely different histories.
318 changed files with 46520 additions and 13878 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
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
|
|
@ -5,7 +5,7 @@ updates:
|
|||
schedule:
|
||||
interval: "daily"
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 3
|
||||
open-pull-requests-limit: 5
|
||||
rebase-strategy: "auto"
|
||||
ignore:
|
||||
- dependency-name: "@playwright/test"
|
||||
|
|
|
|||
84
.github/workflows/dev-checks.yml
vendored
84
.github/workflows/dev-checks.yml
vendored
|
|
@ -5,79 +5,35 @@ on:
|
|||
branches: [ "main" ]
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Check
|
||||
dev-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: Run build
|
||||
run: npm run build
|
||||
|
||||
lint-checks:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js 22
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
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
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Check for manual changes to API
|
||||
run: |
|
||||
npm run api
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo
|
||||
echo "✗ API manually changed, please refer to the README for the procedure to follow for programmatically generated API endpoints."
|
||||
exit 1
|
||||
else
|
||||
echo
|
||||
echo "✓ No manual API changes."
|
||||
exit 0
|
||||
fi
|
||||
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)" ]
|
||||
- name: Check for circular dependencies
|
||||
run: npm run circular
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
- name: Run lint check
|
||||
run: npm run lint
|
||||
- name: Run unit tests
|
||||
run: npm run test:coverage
|
||||
- name: Run unit tests with cockpit
|
||||
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:
|
||||
types: [opened, reopened, synchronize, labeled, unlabeled]
|
||||
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:
|
||||
playwright-tests:
|
||||
|
|
@ -37,7 +30,7 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
|
||||
- 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]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
pr-best-practices:
|
||||
|
|
|
|||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -13,10 +13,10 @@ jobs:
|
|||
# artefact name.
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 22
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
|
|||
1
.github/workflows/stale-cleanup.yml
vendored
1
.github/workflows/stale-cleanup.yml
vendored
|
|
@ -8,7 +8,6 @@ jobs:
|
|||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write # needed to clean up the saved action state
|
||||
issues: write
|
||||
pull-requests: write
|
||||
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/
|
||||
/playwright/.cache/
|
||||
.env
|
||||
.auth
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ test:
|
|||
- RUNNER:
|
||||
- aws/fedora-41-x86_64
|
||||
- aws/fedora-42-x86_64
|
||||
- aws/rhel-10.1-nightly-x86_64
|
||||
- aws/rhel-9.6-nightly-x86_64
|
||||
- aws/rhel-10.0-nightly-x86_64
|
||||
INTERNAL_NETWORK: ["true"]
|
||||
|
||||
finish:
|
||||
|
|
|
|||
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/target_branch: '{{target_branch}}'
|
||||
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/"))
|
||||
creationTimestamp:
|
||||
pipelinesascode.tekton.dev/on-cel-expression: event == "pull_request" && target_branch
|
||||
== "main"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
appstudio.openshift.io/application: insights-image-builder
|
||||
appstudio.openshift.io/component: image-builder-frontend
|
||||
|
|
@ -45,7 +46,7 @@ spec:
|
|||
- name: name
|
||||
value: show-sbom
|
||||
- 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
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -64,7 +65,7 @@ spec:
|
|||
- name: name
|
||||
value: summary
|
||||
- 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
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -83,11 +84,13 @@ spec:
|
|||
name: output-image
|
||||
type: string
|
||||
- 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
|
||||
type: string
|
||||
- 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
|
||||
type: string
|
||||
- default: "false"
|
||||
|
|
@ -107,7 +110,8 @@ spec:
|
|||
name: prefetch-input
|
||||
type: string
|
||||
- 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
|
||||
- default: "false"
|
||||
description: Build a source image.
|
||||
|
|
@ -152,7 +156,7 @@ spec:
|
|||
- name: name
|
||||
value: init
|
||||
- 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:66e90d31e1386bf516fb548cd3e3f0082b5d0234b8b90dbf9e0d4684b70dbe1a
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -169,7 +173,7 @@ spec:
|
|||
- name: name
|
||||
value: git-clone
|
||||
- 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
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -194,7 +198,7 @@ spec:
|
|||
- name: name
|
||||
value: prefetch-dependencies
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:8c649b82a9d228018e5a5d9b844df9fd1db63db33c9b5034586af3a766378de7
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -238,7 +242,7 @@ spec:
|
|||
- name: name
|
||||
value: buildah
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:1799515338c544f6917044398777714c9e0691895231a9d7f456dca75c6f4b65
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -270,7 +274,7 @@ spec:
|
|||
- name: name
|
||||
value: build-image-index
|
||||
- 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:9c95b1fe17db091ae364344ba2006af46648e08486eef1f6fe1b9e3f10866875
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -282,9 +286,7 @@ spec:
|
|||
- name: build-source-image
|
||||
params:
|
||||
- name: BINARY_IMAGE
|
||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||
- name: BINARY_IMAGE_DIGEST
|
||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
||||
value: $(params.output-image)
|
||||
runAfter:
|
||||
- build-image-index
|
||||
taskRef:
|
||||
|
|
@ -292,7 +294,7 @@ spec:
|
|||
- name: name
|
||||
value: source-build
|
||||
- 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:6077f293bd810c2642200f6c531d938a917201861535b7f720d37f7ed7c5d88d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -321,7 +323,7 @@ spec:
|
|||
- name: name
|
||||
value: sast-shell-check
|
||||
- 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:18d594df21cb92cbc409065b25a863492ea7209e2a34045ced69a24a68ca41d8
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -337,8 +339,6 @@ spec:
|
|||
params:
|
||||
- name: image-url
|
||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||
- name: image-digest
|
||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
||||
runAfter:
|
||||
- build-image-index
|
||||
taskRef:
|
||||
|
|
@ -346,7 +346,7 @@ spec:
|
|||
- name: name
|
||||
value: sast-unicode-check
|
||||
- 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:4d5bf6549e42184e462ab7ccfba0153954c65214aa82f319a3215e94e068cded
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -371,7 +371,7 @@ spec:
|
|||
- name: name
|
||||
value: deprecated-image-check
|
||||
- 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
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -393,7 +393,7 @@ spec:
|
|||
- name: name
|
||||
value: clair-scan
|
||||
- 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:68a8fe28527c4469243119a449e2b3a6655f2acac589c069ea6433242da8ed4d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -413,7 +413,7 @@ spec:
|
|||
- name: name
|
||||
value: ecosystem-cert-preflight-checks
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:302828e9d7abc72b8a44fb2b9be068f86c982d8e5f4550b8bf654571d6361ee8
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -435,7 +435,7 @@ spec:
|
|||
- name: name
|
||||
value: sast-snyk-check
|
||||
- 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:d7bdc1b08b384f5db323c88ccd3aab1ea58db1d401ff2b2338f4b984eec44e1b
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -460,7 +460,7 @@ spec:
|
|||
- name: name
|
||||
value: clamav-scan
|
||||
- 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:386c8c3395b44f6eb927dbad72382808b0ae42008f183064ca77cb4cad998442
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -471,10 +471,8 @@ spec:
|
|||
- "false"
|
||||
- name: apply-tags
|
||||
params:
|
||||
- name: IMAGE_URL
|
||||
- name: IMAGE
|
||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||
- name: IMAGE_DIGEST
|
||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
||||
runAfter:
|
||||
- build-image-index
|
||||
taskRef:
|
||||
|
|
@ -482,7 +480,7 @@ spec:
|
|||
- name: name
|
||||
value: apply-tags
|
||||
- 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:1c6f673fe100a49f58aaef62580c8adf0c397790964f4e7bac7fcd3f4d07c92e
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -503,7 +501,7 @@ spec:
|
|||
- name: name
|
||||
value: push-dockerfile
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:c82189e5d331e489cff99f0399f133fd3fad08921bea86747dfa379d1b5c748d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -523,7 +521,7 @@ spec:
|
|||
- name: name
|
||||
value: rpms-signature-scan
|
||||
- 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:80a4562d5f86eb6812f00d4e30e94c1ad27ec937735dc29f5a63e9335676b3dc
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -544,7 +542,7 @@ spec:
|
|||
- name: workspace
|
||||
volumeClaimTemplate:
|
||||
metadata:
|
||||
creationTimestamp:
|
||||
creationTimestamp: null
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ metadata:
|
|||
build.appstudio.redhat.com/commit_sha: '{{revision}}'
|
||||
build.appstudio.redhat.com/target_branch: '{{target_branch}}'
|
||||
pipelinesascode.tekton.dev/max-keep-runs: "3"
|
||||
pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch == "main"
|
||||
creationTimestamp:
|
||||
pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch
|
||||
== "main"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
appstudio.openshift.io/application: insights-image-builder
|
||||
appstudio.openshift.io/component: image-builder-frontend
|
||||
|
|
@ -42,7 +43,7 @@ spec:
|
|||
- name: name
|
||||
value: show-sbom
|
||||
- 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
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -61,7 +62,7 @@ spec:
|
|||
- name: name
|
||||
value: summary
|
||||
- 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
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -80,11 +81,13 @@ spec:
|
|||
name: output-image
|
||||
type: string
|
||||
- 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
|
||||
type: string
|
||||
- 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
|
||||
type: string
|
||||
- default: "false"
|
||||
|
|
@ -104,7 +107,8 @@ spec:
|
|||
name: prefetch-input
|
||||
type: string
|
||||
- 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
|
||||
- default: "false"
|
||||
description: Build a source image.
|
||||
|
|
@ -149,7 +153,7 @@ spec:
|
|||
- name: name
|
||||
value: init
|
||||
- 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:66e90d31e1386bf516fb548cd3e3f0082b5d0234b8b90dbf9e0d4684b70dbe1a
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -166,7 +170,7 @@ spec:
|
|||
- name: name
|
||||
value: git-clone
|
||||
- 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
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -191,7 +195,7 @@ spec:
|
|||
- name: name
|
||||
value: prefetch-dependencies
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:8c649b82a9d228018e5a5d9b844df9fd1db63db33c9b5034586af3a766378de7
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -235,7 +239,7 @@ spec:
|
|||
- name: name
|
||||
value: buildah
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:1799515338c544f6917044398777714c9e0691895231a9d7f456dca75c6f4b65
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -267,7 +271,7 @@ spec:
|
|||
- name: name
|
||||
value: build-image-index
|
||||
- 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:9c95b1fe17db091ae364344ba2006af46648e08486eef1f6fe1b9e3f10866875
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -279,9 +283,7 @@ spec:
|
|||
- name: build-source-image
|
||||
params:
|
||||
- name: BINARY_IMAGE
|
||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||
- name: BINARY_IMAGE_DIGEST
|
||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
||||
value: $(params.output-image)
|
||||
runAfter:
|
||||
- build-image-index
|
||||
taskRef:
|
||||
|
|
@ -289,7 +291,7 @@ spec:
|
|||
- name: name
|
||||
value: source-build
|
||||
- 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:6077f293bd810c2642200f6c531d938a917201861535b7f720d37f7ed7c5d88d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -318,7 +320,7 @@ spec:
|
|||
- name: name
|
||||
value: sast-shell-check
|
||||
- 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:18d594df21cb92cbc409065b25a863492ea7209e2a34045ced69a24a68ca41d8
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -334,8 +336,6 @@ spec:
|
|||
params:
|
||||
- name: image-url
|
||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||
- name: image-digest
|
||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
||||
runAfter:
|
||||
- build-image-index
|
||||
taskRef:
|
||||
|
|
@ -343,7 +343,7 @@ spec:
|
|||
- name: name
|
||||
value: sast-unicode-check
|
||||
- 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:4d5bf6549e42184e462ab7ccfba0153954c65214aa82f319a3215e94e068cded
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -368,7 +368,7 @@ spec:
|
|||
- name: name
|
||||
value: deprecated-image-check
|
||||
- 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
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -390,7 +390,7 @@ spec:
|
|||
- name: name
|
||||
value: clair-scan
|
||||
- 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:68a8fe28527c4469243119a449e2b3a6655f2acac589c069ea6433242da8ed4d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -410,7 +410,7 @@ spec:
|
|||
- name: name
|
||||
value: ecosystem-cert-preflight-checks
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:302828e9d7abc72b8a44fb2b9be068f86c982d8e5f4550b8bf654571d6361ee8
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -432,7 +432,7 @@ spec:
|
|||
- name: name
|
||||
value: sast-snyk-check
|
||||
- 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:d7bdc1b08b384f5db323c88ccd3aab1ea58db1d401ff2b2338f4b984eec44e1b
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -457,7 +457,7 @@ spec:
|
|||
- name: name
|
||||
value: clamav-scan
|
||||
- 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:386c8c3395b44f6eb927dbad72382808b0ae42008f183064ca77cb4cad998442
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -468,10 +468,8 @@ spec:
|
|||
- "false"
|
||||
- name: apply-tags
|
||||
params:
|
||||
- name: IMAGE_URL
|
||||
- name: IMAGE
|
||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||
- name: IMAGE_DIGEST
|
||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
||||
runAfter:
|
||||
- build-image-index
|
||||
taskRef:
|
||||
|
|
@ -479,7 +477,7 @@ spec:
|
|||
- name: name
|
||||
value: apply-tags
|
||||
- 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:1c6f673fe100a49f58aaef62580c8adf0c397790964f4e7bac7fcd3f4d07c92e
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -500,7 +498,7 @@ spec:
|
|||
- name: name
|
||||
value: push-dockerfile
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:c82189e5d331e489cff99f0399f133fd3fad08921bea86747dfa379d1b5c748d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -520,7 +518,7 @@ spec:
|
|||
- name: name
|
||||
value: rpms-signature-scan
|
||||
- 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:80a4562d5f86eb6812f00d4e30e94c1ad27ec937735dc29f5a63e9335676b3dc
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -541,7 +539,7 @@ spec:
|
|||
- name: workspace
|
||||
volumeClaimTemplate:
|
||||
metadata:
|
||||
creationTimestamp:
|
||||
creationTimestamp: null
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -7,7 +7,7 @@ VERSION := $(shell (cd "$(SRCDIR)" && grep "^Version:" cockpit/$(PACKAGE_NAME).s
|
|||
COMMIT = $(shell (cd "$(SRCDIR)" && git rev-parse HEAD))
|
||||
|
||||
# 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_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
|
||||
|
||||
|
|
|
|||
209
README.md
209
README.md
|
|
@ -19,20 +19,16 @@ Frontend code for Image Builder.
|
|||
## Table of Contents
|
||||
1. [How to build and run image-builder-frontend](#frontend-development)
|
||||
1. [Frontend Development](#frontend-development)
|
||||
2. [Image builder as Cockpit plugin](#image-builder-as-cockpit-plugin)
|
||||
3. [Backend Development](#backend-development)
|
||||
2. [API](#api-endpoints)
|
||||
3. [Unleash feature flags](#unleash-feature-flags)
|
||||
4. [File structure](#file-structure)
|
||||
5. [Style Guidelines](#style-guidelines)
|
||||
6. [Test Guidelines](#test-guidelines)
|
||||
7. [Running hosted service Playwright tests](#running-hosted-service-playwright-tests)
|
||||
1. [API](#api-endpoints)
|
||||
2. [Unleash feature flags](#unleash-feature-flags)
|
||||
2. [Backend Development](#backend-development)
|
||||
2. [File structure](#file-structure)
|
||||
3. [Style Guidelines](#style-guidelines)
|
||||
4. [Test Guidelines](#test-guidelines)
|
||||
5. [Running hosted service Playwright tests](#running-hosted-service-playwright-tests)
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -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`
|
||||
|
||||
### Image builder as Cockpit plugin
|
||||
#### Insights proxy (deprecated)
|
||||
|
||||
> [!NOTE]
|
||||
> 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.
|
||||
1. Clone the insights proxy: https://github.com/RedHatInsights/insights-proxy
|
||||
|
||||
#### Cockpit setup
|
||||
To install and setup Cockpit follow guide at: https://cockpit-project.org/running.html
|
||||
2. Setting up the proxy
|
||||
|
||||
#### On-premises image builder installation and configuration
|
||||
To install and configure `osbuild-composer` on your local machine follow our documentation: https://osbuild.org/docs/on-premises/installation/
|
||||
Choose a runner (podman or docker), and point the SPANDX_CONFIG variable to
|
||||
`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.
|
||||
Use this command whenever you need to compile the latest changes in your frontend code.
|
||||
In the image-builder-frontend checkout directory
|
||||
|
||||
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
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
```bash
|
||||
make cockpit/build
|
||||
```
|
||||
The UI should be running on
|
||||
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
|
||||
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 endpoints
|
||||
|
||||
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
|
||||
stored in `/api/config`. Each endpoint has a corresponding empty API slice and generated API
|
||||
slice which are stored in `/src/store`.
|
||||
OpenAPI schema for the endpoints are stored in `/api/schema`. Their
|
||||
corresponding configuration files are stored in `/api/config`. Each endpoint
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
```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
|
||||
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
|
||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile: 'URL_TO_THE_OPENAPI_SCHEMA',
|
||||
schemaFile: '../schema/foobar.json',
|
||||
apiFile: '../../src/store/emptyFoobarApi.ts',
|
||||
apiImport: 'emptyContentSourcesApi',
|
||||
apiImport: 'emptyEdgeApi',
|
||||
outputFile: '../../src/store/foobarApi.ts',
|
||||
exportName: 'foobarApi',
|
||||
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:
|
||||
|
||||
```
|
||||
ignores: [
|
||||
<other ignored files>,
|
||||
'**/foobarApi.ts',
|
||||
]
|
||||
```bash
|
||||
npx @rtk-query/codegen-openapi ./api/config/foobar.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
|
||||
npm run api
|
||||
|
|
@ -191,12 +173,12 @@ npm run api
|
|||
|
||||
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
|
||||
endpoints in the `filterEndpoints` table.
|
||||
|
||||
## Unleash feature flags
|
||||
#### Unleash feature flags
|
||||
|
||||
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
|
||||
|
|
@ -212,7 +194,7 @@ existing flags:
|
|||
|
||||
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:
|
||||
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.
|
||||
|
||||
|
||||
### Cleaning the flags
|
||||
##### Cleaning the flags
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
### 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 cockpit/devel-install
|
||||
```
|
||||
|
||||
```bash
|
||||
make cockpit/build
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### Quick Reference
|
||||
| Directory | Description |
|
||||
| --------- | ----------- |
|
||||
| [`/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 |
|
||||
| [`/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/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 |
|
||||
|
|
@ -242,19 +272,8 @@ we're planning on using.
|
|||
|
||||
## Style Guidelines
|
||||
|
||||
This project uses recommended rule sets rom several plugins:
|
||||
- `@eslint/js`
|
||||
- `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`
|
||||
This project uses eslint's recommended styling guidelines. These rules can be found here:
|
||||
https://eslint.org/docs/rules/
|
||||
|
||||
To run the linter, use:
|
||||
```bash
|
||||
|
|
@ -263,10 +282,16 @@ npm run lint
|
|||
|
||||
Any errors that can be fixed automatically, can be corrected by running:
|
||||
```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
|
||||
|
||||
|
|
@ -344,12 +369,12 @@ Follow these steps to find and paste the certification file into the 'Keychain A
|
|||
npm ci
|
||||
```
|
||||
|
||||
3. Download the Playwright browsers with
|
||||
3. Download the Playwright browsers with
|
||||
```bash
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
4. Start the local development stage server by running
|
||||
4. Start the local development stage server by running
|
||||
```bash
|
||||
npm run start:stage
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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/contentSources.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/composerCloudApi.ts &
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile: 'https://console.redhat.com/api/compliance/v2/openapi.json',
|
||||
schemaFile: '../schema/compliance.json',
|
||||
apiFile: '../../src/store/service/emptyComplianceApi.ts',
|
||||
apiImport: 'emptyComplianceApi',
|
||||
outputFile: '../../src/store/service/complianceApi.ts',
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile:
|
||||
'https://raw.githubusercontent.com/osbuild/osbuild-composer/main/internal/cloudapi/v2/openapi.v2.yml',
|
||||
schemaFile: '../schema/composerCloudApi.v2.yaml',
|
||||
apiFile: '../../src/store/cockpit/emptyComposerCloudApi.ts',
|
||||
apiImport: 'emptyComposerCloudApi',
|
||||
outputFile: '../../src/store/cockpit/composerCloudApi.ts',
|
||||
exportName: 'composerCloudApi',
|
||||
hooks: false,
|
||||
unionUndefined: true,
|
||||
filterEndpoints: ['postCompose', 'getComposeStatus'],
|
||||
filterEndpoints: [
|
||||
'postCompose',
|
||||
'getComposeStatus',
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile: 'https://console.redhat.com/api/content-sources/v1/openapi.json',
|
||||
schemaFile: '../schema/contentSources.json',
|
||||
apiFile: '../../src/store/service/emptyContentSourcesApi.ts',
|
||||
apiImport: 'emptyContentSourcesApi',
|
||||
outputFile: '../../src/store/service/contentSourcesApi.ts',
|
||||
|
|
@ -12,7 +12,6 @@ const config: ConfigFile = {
|
|||
'createRepository',
|
||||
'listRepositories',
|
||||
'listRepositoriesRpms',
|
||||
'listRepositoryParameters',
|
||||
'searchRpm',
|
||||
'searchPackageGroup',
|
||||
'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';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile:
|
||||
'https://raw.githubusercontent.com/osbuild/image-builder/main/internal/v1/api.yaml',
|
||||
schemaFile: '../schema/imageBuilder.yaml',
|
||||
apiFile: '../../src/store/service/emptyImageBuilderApi.ts',
|
||||
apiImport: 'emptyImageBuilderApi',
|
||||
outputFile: '../../src/store/service/imageBuilderApi.ts',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile: 'https://console.redhat.com/api/provisioning/v1/openapi.json',
|
||||
schemaFile: '../schema/provisioning.json',
|
||||
apiFile: '../../src/store/service/emptyProvisioningApi.ts',
|
||||
apiImport: 'emptyProvisioningApi',
|
||||
outputFile: '../../src/store/service/provisioningApi.ts',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile: 'https://console.redhat.com/api/rhsm/v2/openapi.json',
|
||||
schemaFile: '../schema/rhsm.json',
|
||||
apiFile: '../../src/store/service/emptyRhsmApi.ts',
|
||||
apiImport: 'emptyRhsmApi',
|
||||
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 APP_ROOT=$(pwd)
|
||||
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
|
||||
|
||||
set -exv
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
# cockpit-image-builder
|
||||
|
||||
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.
|
||||
TODO
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
Name: cockpit-image-builder
|
||||
Version: 76
|
||||
Version: 69
|
||||
Release: 1%{?dist}
|
||||
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">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Image-Builder</title>
|
||||
|
||||
<!-- js dependencies -->
|
||||
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
||||
<script defer src="main.js"></script>
|
||||
<link href="main.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="ct-page-fill" id="main"></div>
|
||||
</body>
|
||||
<link href="main.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script defer src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -75,15 +75,7 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: { url: false },
|
||||
},
|
||||
'sass-loader',
|
||||
],
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', '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
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
]);
|
||||
4192
package-lock.json
generated
4192
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": {
|
||||
"@ltd/j-toml": "1.38.0",
|
||||
"@patternfly/patternfly": "6.3.1",
|
||||
"@patternfly/react-code-editor": "6.3.1",
|
||||
"@patternfly/react-core": "6.3.1",
|
||||
"@patternfly/react-table": "6.3.1",
|
||||
"@redhat-cloud-services/frontend-components": "7.0.3",
|
||||
"@redhat-cloud-services/frontend-components-notifications": "6.1.5",
|
||||
"@redhat-cloud-services/frontend-components-utilities": "7.0.3",
|
||||
"@redhat-cloud-services/types": "3.0.1",
|
||||
"@patternfly/patternfly": "6.1.0",
|
||||
"@patternfly/react-code-editor": "6.1.0",
|
||||
"@patternfly/react-core": "6.1.0",
|
||||
"@patternfly/react-table": "6.1.0",
|
||||
"@redhat-cloud-services/frontend-components": "6.0.4",
|
||||
"@redhat-cloud-services/frontend-components-notifications": "5.0.4",
|
||||
"@redhat-cloud-services/frontend-components-utilities": "6.0.2",
|
||||
"@reduxjs/toolkit": "2.8.2",
|
||||
"@scalprum/react-core": "0.9.5",
|
||||
"@sentry/webpack-plugin": "4.1.1",
|
||||
"@unleash/proxy-client-react": "5.0.1",
|
||||
"@sentry/webpack-plugin": "3.5.0",
|
||||
"@unleash/proxy-client-react": "5.0.0",
|
||||
"classnames": "2.5.1",
|
||||
"jwt-decode": "4.0.0",
|
||||
"lodash": "4.17.21",
|
||||
|
|
@ -31,72 +30,68 @@
|
|||
"redux-promise-middleware": "6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.0",
|
||||
"@babel/preset-env": "7.28.0",
|
||||
"@babel/core": "7.26.10",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@currents/playwright": "1.15.3",
|
||||
"@eslint/js": "9.32.0",
|
||||
"@patternfly/react-icons": "6.3.1",
|
||||
"@babel/preset-typescript": "7.27.0",
|
||||
"@currents/playwright": "1.13.2",
|
||||
"@patternfly/react-icons": "6.1.0",
|
||||
"@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/tsc-transform-imports": "1.0.25",
|
||||
"@redhat-cloud-services/tsc-transform-imports": "1.0.24",
|
||||
"@rtk-query/codegen-openapi": "2.0.0",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "6.6.4",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/node": "24.3.0",
|
||||
"@types/node": "22.15.1",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-redux": "7.1.34",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||
"@typescript-eslint/parser": "8.32.1",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"@vitest/coverage-v8": "3.1.2",
|
||||
"babel-loader": "10.0.0",
|
||||
"chart.js": "4.5.0",
|
||||
"chart.js": "4.4.9",
|
||||
"chartjs-adapter-moment": "1.0.1",
|
||||
"chartjs-plugin-annotation": "3.1.0",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"css-loader": "7.1.2",
|
||||
"eslint": "9.33.0",
|
||||
"eslint": "8.57.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-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-playwright": "2.2.2",
|
||||
"eslint-plugin-prettier": "5.5.4",
|
||||
"eslint-plugin-playwright": "2.2.0",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"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",
|
||||
"globals": "16.3.0",
|
||||
"history": "5.3.0",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"jsdom": "26.1.0",
|
||||
"madge": "8.0.0",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"moment": "2.30.1",
|
||||
"msw": "2.10.5",
|
||||
"msw": "2.7.5",
|
||||
"npm-run-all": "4.1.5",
|
||||
"path-browserify": "1.0.1",
|
||||
"postcss-scss": "4.0.9",
|
||||
"react-chartjs-2": "5.3.0",
|
||||
"redux-mock-store": "1.5.5",
|
||||
"sass": "1.90.0",
|
||||
"sass": "1.88.0",
|
||||
"sass-loader": "16.0.5",
|
||||
"stylelint": "16.23.1",
|
||||
"stylelint-config-recommended-scss": "16.0.0",
|
||||
"stylelint": "16.18.0",
|
||||
"stylelint-config-recommended-scss": "14.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"ts-patch": "3.3.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.40.0",
|
||||
"uuid": "11.1.0",
|
||||
"vitest": "3.2.4",
|
||||
"vitest": "3.1.2",
|
||||
"vitest-canvas-mock": "0.3.3",
|
||||
"webpack-bundle-analyzer": "4.10.2",
|
||||
"whatwg-fetch": "3.6.20"
|
||||
|
|
@ -117,7 +112,9 @@
|
|||
"test:cockpit": "src/test/cockpit-tests.sh",
|
||||
"build": "fec build",
|
||||
"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",
|
||||
"postinstall": "ts-patch install",
|
||||
"circular": "madge --circular ./src --extensions js,ts,tsx",
|
||||
|
|
|
|||
10
packit.yaml
10
packit.yaml
|
|
@ -16,15 +16,6 @@ srpm_build_deps:
|
|||
- npm
|
||||
|
||||
jobs:
|
||||
- job: tests
|
||||
identifier: self
|
||||
trigger: pull_request
|
||||
tmt_plan: /plans/all/main
|
||||
targets:
|
||||
- centos-stream-10
|
||||
- fedora-41
|
||||
- fedora-42
|
||||
|
||||
- job: copr_build
|
||||
trigger: pull_request
|
||||
targets: &build_targets
|
||||
|
|
@ -33,6 +24,7 @@ jobs:
|
|||
- centos-stream-10
|
||||
- centos-stream-10-aarch64
|
||||
- fedora-all
|
||||
- fedora-all-aarch64
|
||||
|
||||
- job: copr_build
|
||||
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,
|
||||
},
|
||||
projects: [
|
||||
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: '.auth/user.json',
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} from '../helpers/navHelpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import {
|
||||
registerLater,
|
||||
fillInDetails,
|
||||
createBlueprint,
|
||||
fillInImageOutputGuest,
|
||||
deleteBlueprint,
|
||||
exportBlueprint,
|
||||
fillInDetails,
|
||||
fillInImageOutputGuest,
|
||||
importBlueprint,
|
||||
registerLater,
|
||||
} from '../helpers/wizardHelpers';
|
||||
|
||||
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
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
await ensureAuthenticated(page);
|
||||
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
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 frame.getByPlaceholder('Add ports').fill('x');
|
||||
await frame.getByRole('button', { name: 'Add ports' }).click();
|
||||
await expect(
|
||||
frame
|
||||
.getByText(
|
||||
'Expected format: <port/port-name>:<protocol>. Example: 8080:tcp, ssh:tcp',
|
||||
)
|
||||
.nth(0),
|
||||
).toBeVisible();
|
||||
await expect(frame.getByText('Invalid format.').nth(0)).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Select and incorrectly fill the disabled services in Firewall step', async () => {
|
||||
await frame.getByPlaceholder('Add disabled service').fill('1');
|
||||
await frame.getByRole('button', { name: 'Add disabled service' }).click();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(0),
|
||||
).toBeVisible();
|
||||
await expect(frame.getByText('Invalid format.').nth(1)).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Select and incorrectly fill the enabled services in Firewall step', async () => {
|
||||
await frame.getByPlaceholder('Add enabled service').fill('ťčš');
|
||||
await frame.getByRole('button', { name: 'Add enabled service' }).click();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(1),
|
||||
).toBeVisible();
|
||||
await expect(frame.getByText('Invalid format.').nth(2)).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Fill the BP details', async () => {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} from '../helpers/navHelpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import {
|
||||
registerLater,
|
||||
fillInDetails,
|
||||
createBlueprint,
|
||||
fillInImageOutputGuest,
|
||||
deleteBlueprint,
|
||||
exportBlueprint,
|
||||
fillInDetails,
|
||||
fillInImageOutputGuest,
|
||||
importBlueprint,
|
||||
registerLater,
|
||||
} from '../helpers/wizardHelpers';
|
||||
|
||||
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
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
await ensureAuthenticated(page);
|
||||
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
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 page.getByRole('button', { name: 'Hostname' }).click();
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'hostname input' }),
|
||||
page.getByRole('textbox', { name: 'hostname input' })
|
||||
).toHaveValue(hostname + 'edited');
|
||||
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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} from '../helpers/navHelpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import {
|
||||
registerLater,
|
||||
fillInDetails,
|
||||
createBlueprint,
|
||||
fillInImageOutputGuest,
|
||||
deleteBlueprint,
|
||||
exportBlueprint,
|
||||
fillInDetails,
|
||||
fillInImageOutputGuest,
|
||||
importBlueprint,
|
||||
registerLater,
|
||||
} from '../helpers/wizardHelpers';
|
||||
|
||||
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
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
await ensureAuthenticated(page);
|
||||
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
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 frame.getByRole('button', { name: 'Locale' }).click();
|
||||
await frame.getByPlaceholder('Select a language').fill('fy');
|
||||
await frame
|
||||
.getByRole('option', { name: 'Western Frisian - Germany (fy_DE.UTF-8)' })
|
||||
.click();
|
||||
await frame.getByRole('option', { name: 'fy_DE.UTF-' }).click();
|
||||
await expect(
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||
).toBeEnabled();
|
||||
await frame
|
||||
.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
})
|
||||
.click();
|
||||
await frame.getByRole('button', { name: 'Close fy_DE.UTF-' }).click();
|
||||
await expect(
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||
).toBeHidden();
|
||||
await frame.getByPlaceholder('Select a language').fill('fy');
|
||||
await frame
|
||||
.getByRole('option', { name: 'Western Frisian - Germany (fy_DE.UTF-8)' })
|
||||
.click();
|
||||
await frame.getByRole('option', { name: 'fy_DE.UTF-' }).click();
|
||||
await expect(
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||
).toBeEnabled();
|
||||
await frame.getByPlaceholder('Select a language').fill('aa');
|
||||
await frame
|
||||
.getByRole('option', { name: 'aa - Djibouti (aa_DJ.UTF-8)' })
|
||||
.click();
|
||||
await frame.getByRole('option', { name: 'aa_DJ.UTF-' }).click();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
|
||||
frame.getByRole('button', { name: 'Close aa_DJ.UTF-' })
|
||||
).toBeEnabled();
|
||||
await frame.getByPlaceholder('Select a language').fill('aa');
|
||||
await expect(
|
||||
frame.getByText(
|
||||
'aa - Djibouti (aa_DJ.UTF-8)Language already addedaa - Eritrea (aa_ER.UTF-8)aa - Ethiopia (aa_ET.UTF-8)',
|
||||
),
|
||||
frame.getByText('aa_DJ.UTF-8Language already addedaa_ER.UTF-8aa_ET.UTF-')
|
||||
).toBeAttached();
|
||||
await frame.getByPlaceholder('Select a language').fill('xxx');
|
||||
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.getByLabel('Revisit Locale step').click();
|
||||
await expect(
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||
).toBeEnabled();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
|
||||
frame.getByRole('button', { name: 'Close aa_DJ.UTF-' })
|
||||
).toBeEnabled();
|
||||
await frame.getByPlaceholder('Select a language').fill('aa');
|
||||
await frame
|
||||
.getByRole('option', { name: 'aa - Eritrea (aa_ER.UTF-8)' })
|
||||
.click();
|
||||
await frame.getByRole('option', { name: 'aa_ER.UTF-' }).click();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa - Eritrea (aa_ER.UTF-8)' }),
|
||||
frame.getByRole('button', { name: 'Close aa_ER.UTF-' })
|
||||
).toBeEnabled();
|
||||
await frame.getByRole('button', { name: 'Clear input' }).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 page.getByRole('button', { name: 'Locale' }).click();
|
||||
await expect(
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||
).toBeEnabled();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
|
||||
frame.getByRole('button', { name: 'Close aa_DJ.UTF-' })
|
||||
).toBeEnabled();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa - Eritrea (aa_ER.UTF-8)' }),
|
||||
frame.getByRole('button', { name: 'Close aa_ER.UTF-' })
|
||||
).toBeEnabled();
|
||||
await expect(frame.getByPlaceholder('Select a keyboard')).toHaveValue(
|
||||
'ANSI-dvorak',
|
||||
'ANSI-dvorak'
|
||||
);
|
||||
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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} from '../helpers/navHelpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import {
|
||||
registerLater,
|
||||
fillInDetails,
|
||||
createBlueprint,
|
||||
fillInImageOutputGuest,
|
||||
deleteBlueprint,
|
||||
exportBlueprint,
|
||||
fillInDetails,
|
||||
fillInImageOutputGuest,
|
||||
importBlueprint,
|
||||
registerLater,
|
||||
} from '../helpers/wizardHelpers';
|
||||
|
||||
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
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
await ensureAuthenticated(page);
|
||||
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
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 frame.getByPlaceholder('Add disabled service').fill('&&');
|
||||
await frame.getByRole('button', { name: 'Add disabled service' }).click();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(0),
|
||||
).toBeVisible();
|
||||
await expect(frame.getByText('Invalid format.').nth(0)).toBeVisible();
|
||||
|
||||
await frame.getByPlaceholder('Add enabled service').fill('áá');
|
||||
await frame.getByRole('button', { name: 'Add enabled service' }).click();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(1),
|
||||
).toBeVisible();
|
||||
await expect(frame.getByText('Invalid format.').nth(1)).toBeVisible();
|
||||
|
||||
await frame.getByPlaceholder('Add masked service').fill('78');
|
||||
await frame.getByRole('button', { name: 'Add masked service' }).click();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(2),
|
||||
).toBeVisible();
|
||||
await expect(frame.getByText('Invalid format.').nth(2)).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Fill the BP details', async () => {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} from '../helpers/navHelpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import {
|
||||
registerLater,
|
||||
fillInDetails,
|
||||
createBlueprint,
|
||||
fillInImageOutputGuest,
|
||||
deleteBlueprint,
|
||||
exportBlueprint,
|
||||
fillInDetails,
|
||||
fillInImageOutputGuest,
|
||||
importBlueprint,
|
||||
registerLater,
|
||||
} from '../helpers/wizardHelpers';
|
||||
|
||||
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
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
await ensureAuthenticated(page);
|
||||
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
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 frame.getByPlaceholder('Add NTP servers').fill('xxxx');
|
||||
await frame.getByRole('button', { name: 'Add NTP server' }).click();
|
||||
await expect(
|
||||
frame
|
||||
.getByText('Expected format: <ntp-server>. Example: time.redhat.com')
|
||||
.nth(0),
|
||||
).toBeVisible();
|
||||
await expect(frame.getByText('Invalid format.')).toBeVisible();
|
||||
await frame.getByPlaceholder('Add NTP servers').fill('0.cz.pool.ntp.org');
|
||||
await frame.getByRole('button', { name: 'Add NTP server' }).click();
|
||||
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 expect(frame.getByText('Canada/Saskatchewan')).toBeHidden();
|
||||
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
|
||||
'Europe/Stockholm',
|
||||
'Europe/Stockholm'
|
||||
);
|
||||
await frame.getByPlaceholder('Select a timezone').fill('Europe');
|
||||
await frame.getByRole('option', { name: 'Europe/Oslo' }).click();
|
||||
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.de.pool.ntp.org')).toBeVisible();
|
||||
|
|
@ -118,7 +108,7 @@ test('Create a blueprint with Timezone customization', async ({
|
|||
await fillInImageOutputGuest(page);
|
||||
await frame.getByRole('button', { name: 'Timezone' }).click();
|
||||
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.de.pool.ntp.org')).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface Cleanup {
|
|||
}
|
||||
|
||||
export const test = oldTest.extend<WithCleanup>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
cleanup: async ({}, use) => {
|
||||
const cleanupFns: Map<symbol, () => Promise<unknown>> = new Map();
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ export const test = oldTest.extend<WithCleanup>({
|
|||
async () => {
|
||||
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 { readFileSync } from 'node:fs';
|
||||
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
import { type Page, expect } from '@playwright/test';
|
||||
|
||||
export const togglePreview = async (page: Page) => {
|
||||
const toggleSwitch = page.locator('#preview-toggle');
|
||||
|
|
@ -31,57 +28,12 @@ export const closePopupsIfExist = async (page: Page) => {
|
|||
page
|
||||
.locator('iframe[name="intercom-modal-frame"]')
|
||||
.contentFrame()
|
||||
.getByRole('button', { name: 'Close' }), // This closes the intercom pop-up
|
||||
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)
|
||||
.getByRole('button', { name: 'Close' }),
|
||||
];
|
||||
|
||||
for (const locator of locatorsToCheck) {
|
||||
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 { expect, type Page } from '@playwright/test';
|
||||
import { type Page, expect } from '@playwright/test';
|
||||
|
||||
import { closePopupsIfExist, isHosted, togglePreview } from './helpers';
|
||||
import { ibFrame } from './navHelpers';
|
||||
|
|
@ -23,38 +21,6 @@ export const login = async (page: Page) => {
|
|||
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) => {
|
||||
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
|
||||
const frame = ibFrame(page);
|
||||
|
||||
try {
|
||||
// Check if the user already has administrative access
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Administrative access' }),
|
||||
).toBeVisible();
|
||||
} catch {
|
||||
// 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();
|
||||
// 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)
|
||||
const authenticateButton = page.getByRole('button', {
|
||||
name: 'Authenticate',
|
||||
});
|
||||
const closeButton = page.getByText('Close');
|
||||
await expect(authenticateButton.or(closeButton)).toBeVisible();
|
||||
// different popup opens based on type of account (can be passwordless)
|
||||
const authenticateButton = page.getByRole('button', { name: 'Authenticate' });
|
||||
const closeButton = page.getByText('Close');
|
||||
await expect(authenticateButton.or(closeButton)).toBeVisible();
|
||||
|
||||
if (await authenticateButton.isVisible()) {
|
||||
// with password
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill(password);
|
||||
await authenticateButton.click();
|
||||
}
|
||||
if (await closeButton.isVisible()) {
|
||||
// passwordless
|
||||
await closeButton.click();
|
||||
}
|
||||
if (await authenticateButton.isVisible()) {
|
||||
// with password
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill(password);
|
||||
await authenticateButton.click();
|
||||
}
|
||||
if (await closeButton.isVisible()) {
|
||||
// passwordless
|
||||
await closeButton.click();
|
||||
}
|
||||
|
||||
// expect to have administrative access
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Administrative access' }),
|
||||
page.getByRole('button', { name: 'Administrative access' })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
frame.getByRole('heading', { name: 'All images' }),
|
||||
frame.getByRole('heading', { name: 'All images' })
|
||||
).toBeVisible();
|
||||
};
|
||||
|
||||
const loginConsole = async (page: Page, user: string, password: string) => {
|
||||
await closePopupsIfExist(page);
|
||||
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('textbox', { name: 'Password' }).fill(password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await togglePreview(page);
|
||||
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
|
||||
* @param page - the page object
|
||||
*/
|
||||
export const navigateToOptionalSteps = async (page: Page | FrameLocator) => {
|
||||
await page.getByRole('button', { name: 'Create image 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('button', { name: 'Create blueprint' }).click();
|
||||
await page.getByRole('checkbox', { name: 'Virtualization' }).click();
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, FrameLocator, type Page, test } from '@playwright/test';
|
||||
|
||||
import { closePopupsIfExist, isHosted } from './helpers';
|
||||
import { isHosted } from './helpers';
|
||||
import { ibFrame, navigateToLandingPage } from './navHelpers';
|
||||
|
||||
/**
|
||||
|
|
@ -10,15 +10,13 @@ import { ibFrame, navigateToLandingPage } from './navHelpers';
|
|||
*/
|
||||
export const createBlueprint = async (
|
||||
page: Page | FrameLocator,
|
||||
blueprintName: string,
|
||||
blueprintName: string
|
||||
) => {
|
||||
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
||||
await page.getByRole('button', { name: 'Close' }).first().click();
|
||||
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
||||
await page.getByRole('textbox', { name: 'Search input' }).fill(blueprintName);
|
||||
// the clickable blueprint cards are a bit awkward, so use the
|
||||
// button's id instead
|
||||
await page.locator(`button[id="${blueprintName}"]`).click();
|
||||
await page.getByTestId('blueprint-card').getByText(blueprintName).click();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -31,7 +29,7 @@ export const createBlueprint = async (
|
|||
*/
|
||||
export const fillInDetails = async (
|
||||
page: Page | FrameLocator,
|
||||
blueprintName: string,
|
||||
blueprintName: string
|
||||
) => {
|
||||
await page.getByRole('listitem').filter({ hasText: 'Details' }).click();
|
||||
await page
|
||||
|
|
@ -72,8 +70,6 @@ export const fillInImageOutputGuest = async (page: Page | FrameLocator) => {
|
|||
* @param blueprintName - the name of the blueprint to delete
|
||||
*/
|
||||
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(
|
||||
'Delete the blueprint with name: ' + blueprintName,
|
||||
async () => {
|
||||
|
|
@ -86,22 +82,22 @@ export const deleteBlueprint = async (page: Page, blueprintName: string) => {
|
|||
// Check if no blueprints found -> that means no blueprint was created -> fail gracefully and do not raise error
|
||||
try {
|
||||
await expect(
|
||||
frame.getByRole('heading', { name: 'No blueprints found' }),
|
||||
frame.getByRole('heading', { name: 'No blueprints found' })
|
||||
).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
|
||||
.getByTestId('blueprint-card')
|
||||
.getByText(blueprintName)
|
||||
.click();
|
||||
await frame.getByRole('button', { name: 'Menu toggle' }).click();
|
||||
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).click();
|
||||
await frame.getByRole('button', { name: 'Delete' }).click();
|
||||
},
|
||||
{ box: true },
|
||||
{ box: true }
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -129,7 +125,7 @@ export const exportBlueprint = async (page: Page, blueprintName: string) => {
|
|||
*/
|
||||
export const importBlueprint = async (
|
||||
page: Page | FrameLocator,
|
||||
blueprintName: string,
|
||||
blueprintName: string
|
||||
) => {
|
||||
if (isHosted()) {
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
|
|
@ -138,7 +134,7 @@ export const importBlueprint = async (
|
|||
.locator('input[type=file]')
|
||||
.setInputFiles('../../downloads/' + blueprintName + '.json');
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'File upload' }),
|
||||
page.getByRole('textbox', { name: 'File upload' })
|
||||
).not.toBeEmpty();
|
||||
await page.getByRole('button', { name: 'Review and Finish' }).click();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import TOML from '@ltd/j-toml';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { closePopupsIfExist, isHosted } from './helpers/helpers';
|
||||
import { ensureAuthenticated } from './helpers/login';
|
||||
import { ibFrame, navigateToLandingPage } from './helpers/navHelpers';
|
||||
import { isHosted } from './helpers/helpers';
|
||||
import { login } from './helpers/login';
|
||||
import { ibFrame } from './helpers/navHelpers';
|
||||
|
||||
test.describe.serial('test', () => {
|
||||
const blueprintName = uuidv4();
|
||||
test('create blueprint', async ({ page }) => {
|
||||
await ensureAuthenticated(page);
|
||||
await closePopupsIfExist(page);
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
frame.getByRole('heading', { name: 'Images About image builder' });
|
||||
|
|
@ -22,16 +16,14 @@ test.describe.serial('test', () => {
|
|||
await frame.getByTestId('blueprints-create-button').click();
|
||||
|
||||
frame.getByRole('heading', { name: 'Image output' });
|
||||
await frame
|
||||
.getByRole('checkbox', { name: /Virtualization guest image/i })
|
||||
.click();
|
||||
await frame.getByTestId('checkbox-guest-image').click();
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
|
||||
if (isHosted()) {
|
||||
frame.getByRole('heading', {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -72,11 +64,6 @@ test.describe.serial('test', () => {
|
|||
frame.getByRole('heading', { name: 'Systemd services' });
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
|
||||
if (isHosted()) {
|
||||
frame.getByRole('heading', { name: 'Ansible Automation Platform' });
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
}
|
||||
|
||||
if (isHosted()) {
|
||||
frame.getByRole('heading', { name: 'First boot configuration' });
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
|
|
@ -92,32 +79,22 @@ test.describe.serial('test', () => {
|
|||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
||||
|
||||
await expect(
|
||||
frame.locator('.pf-v6-c-card__title-text').getByText(
|
||||
// if the name is too long, the blueprint card will have a truncated name.
|
||||
blueprintName.length > 24
|
||||
? blueprintName.slice(0, 24) + '...'
|
||||
: blueprintName,
|
||||
),
|
||||
frame.locator('.pf-v6-c-card__title-text').getByText(blueprintName)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('edit blueprint', async ({ page }) => {
|
||||
await ensureAuthenticated(page);
|
||||
await closePopupsIfExist(page);
|
||||
// package searching is really slow the first time in cockpit
|
||||
if (!isHosted()) {
|
||||
test.setTimeout(300000);
|
||||
}
|
||||
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'Search input' })
|
||||
.fill(blueprintName);
|
||||
// the clickable blueprint cards are a bit awkward, so use the
|
||||
// button's id instead
|
||||
await frame.locator(`button[id="${blueprintName}"]`).click();
|
||||
await frame.getByText(blueprintName, { exact: true }).first().click();
|
||||
|
||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
||||
await frame.getByRole('button', { name: 'Additional packages' }).click();
|
||||
|
|
@ -131,7 +108,6 @@ test.describe.serial('test', () => {
|
|||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||
await frame.getByRole('button', { name: 'About packages' }).click();
|
||||
frame.getByRole('gridcell', { name: 'osbuild-composer' });
|
||||
await frame.getByRole('button', { name: 'Close', exact: true }).click();
|
||||
await frame
|
||||
.getByRole('button', { name: 'Save changes to blueprint' })
|
||||
.click();
|
||||
|
|
@ -139,23 +115,17 @@ test.describe.serial('test', () => {
|
|||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
||||
await frame.getByRole('button', { name: 'About packages' }).click();
|
||||
frame.getByRole('gridcell', { name: 'osbuild-composer' });
|
||||
await frame.getByRole('button', { name: 'Close', exact: true }).click();
|
||||
await frame.getByRole('button', { name: 'Cancel', exact: true }).click();
|
||||
frame.getByRole('heading', { name: 'All images' });
|
||||
});
|
||||
|
||||
test('build blueprint', async ({ page }) => {
|
||||
await ensureAuthenticated(page);
|
||||
await closePopupsIfExist(page);
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'Search input' })
|
||||
.fill(blueprintName);
|
||||
// the clickable blueprint cards are a bit awkward, so use the
|
||||
// button's id instead
|
||||
await frame.locator(`button[id="${blueprintName}"]`).click();
|
||||
await frame.getByText(blueprintName, { exact: true }).first().click();
|
||||
await frame.getByTestId('blueprint-build-image-menu-option').click();
|
||||
|
||||
// make sure the image is present
|
||||
|
|
@ -167,148 +137,14 @@ test.describe.serial('test', () => {
|
|||
});
|
||||
|
||||
test('delete blueprint', async ({ page }) => {
|
||||
await ensureAuthenticated(page);
|
||||
await closePopupsIfExist(page);
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'Search input' })
|
||||
.fill(blueprintName);
|
||||
// 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: /blueprint menu toggle/i }).click();
|
||||
await frame.getByText(blueprintName, { exact: true }).first().click();
|
||||
await frame.getByTestId('blueprint-action-menu-toggle').click();
|
||||
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).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
|
||||
set -euo pipefail
|
||||
|
||||
TMT_SOURCE_DIR=${TMT_SOURCE_DIR:-}
|
||||
if [ -n "$TMT_SOURCE_DIR" ]; then
|
||||
# Move to the directory with sources
|
||||
cd "${TMT_SOURCE_DIR}/cockpit-image-builder"
|
||||
npm ci
|
||||
elif [ "${CI:-}" != "true" ]; then
|
||||
# packit drops us into the schutzbot directory
|
||||
cd ../
|
||||
npm ci
|
||||
fi
|
||||
# As playwright isn't supported on fedora/el, install dependencies
|
||||
# beforehand.
|
||||
sudo dnf install -y \
|
||||
alsa-lib \
|
||||
libXrandr-devel \
|
||||
libXdamage-devel \
|
||||
libXcomposite-devel \
|
||||
at-spi2-atk-devel \
|
||||
cups \
|
||||
atk
|
||||
|
||||
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"
|
||||
|
||||
function upload_artifacts {
|
||||
if [ -n "${TMT_TEST_DATA:-}" ]; then
|
||||
mv playwright-report "$TMT_TEST_DATA"/playwright-report
|
||||
else
|
||||
USER="$(whoami)"
|
||||
sudo chown -R "$USER:$USER" playwright-report
|
||||
mv playwright-report /tmp/artifacts/
|
||||
fi
|
||||
mkdir -p /tmp/artifacts/extra-screenshots
|
||||
USER="$(whoami)"
|
||||
sudo chown -R "$USER:$USER" playwright-report
|
||||
mv playwright-report /tmp/artifacts/
|
||||
}
|
||||
trap upload_artifacts EXIT
|
||||
|
||||
|
|
@ -76,12 +73,10 @@ sudo podman run \
|
|||
-e "CI=true" \
|
||||
-e "PLAYWRIGHT_USER=admin" \
|
||||
-e "PLAYWRIGHT_PASSWORD=foobar" \
|
||||
-e "CURRENTS_PROJECT_ID=${CURRENTS_PROJECT_ID:-}" \
|
||||
-e "CURRENTS_RECORD_KEY=${CURRENTS_RECORD_KEY:-}" \
|
||||
-e "CURRENTS_PROJECT_ID=$CURRENTS_PROJECT_ID" \
|
||||
-e "CURRENTS_RECORD_KEY=$CURRENTS_RECORD_KEY" \
|
||||
--net=host \
|
||||
-v "$PWD:/tests" \
|
||||
-v '/etc:/etc' \
|
||||
-v '/etc/os-release:/etc/os-release' \
|
||||
--privileged \
|
||||
--rm \
|
||||
--init \
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
cf0a810fd3b75fa27139746c4dfe72222e13dcba
|
||||
7b4735d287dd0950e0a6f47dde65b62b0f239da1
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import NotificationsProvider from '@redhat-cloud-services/frontend-components-notifications/NotificationsProvider';
|
||||
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import NotificationsPortal from '@redhat-cloud-services/frontend-components-notifications/NotificationPortal';
|
||||
import '@patternfly/patternfly/patternfly-addons.css';
|
||||
|
||||
import { Router } from './Router';
|
||||
|
|
@ -26,9 +26,8 @@ const App = () => {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NotificationsProvider>
|
||||
<Router />
|
||||
</NotificationsProvider>
|
||||
<NotificationsPortal />
|
||||
<Router />
|
||||
</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 'cockpit-dark-theme';
|
||||
import { Page, PageSection } from '@patternfly/react-core';
|
||||
import NotificationsProvider from '@redhat-cloud-services/frontend-components-notifications/NotificationsProvider';
|
||||
import NotificationsPortal from '@redhat-cloud-services/frontend-components-notifications/NotificationPortal';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import './AppCockpit.scss';
|
||||
import { NotReady, RequireAdmin } from './Components/Cockpit';
|
||||
import { Router } from './Router';
|
||||
import { onPremStore as store } from './store';
|
||||
|
|
@ -31,21 +28,16 @@ const Application = () => {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NotificationsProvider>
|
||||
<HashRouter>
|
||||
<Router />
|
||||
</HashRouter>
|
||||
</NotificationsProvider>
|
||||
<NotificationsPortal />
|
||||
<HashRouter>
|
||||
<Router />
|
||||
</HashRouter>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const ImageBuilder = () => (
|
||||
<Provider store={store}>
|
||||
<Page className='no-masthead-sidebar' isContentFilled>
|
||||
<PageSection>
|
||||
<Application />
|
||||
</PageSection>
|
||||
</Page>
|
||||
<Application />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const BlueprintActionsMenu: React.FunctionComponent<
|
|||
setShowBlueprintActionsMenu(!showBlueprintActionsMenu);
|
||||
};
|
||||
const importExportFlag = useFlagWithEphemDefault(
|
||||
'image-builder.import.enabled',
|
||||
'image-builder.import.enabled'
|
||||
);
|
||||
|
||||
const [trigger] = useLazyExportBlueprintQuery();
|
||||
|
|
@ -58,10 +58,11 @@ export const BlueprintActionsMenu: React.FunctionComponent<
|
|||
ref={toggleRef}
|
||||
isExpanded={showBlueprintActionsMenu}
|
||||
onClick={() => setShowBlueprintActionsMenu(!showBlueprintActionsMenu)}
|
||||
variant='plain'
|
||||
aria-label='blueprint menu toggle'
|
||||
variant="plain"
|
||||
aria-label="blueprint menu toggle"
|
||||
data-testid="blueprint-action-menu-toggle"
|
||||
>
|
||||
<EllipsisVIcon aria-hidden='true' />
|
||||
<EllipsisVIcon aria-hidden="true" />
|
||||
</MenuToggle>
|
||||
)}
|
||||
>
|
||||
|
|
@ -81,7 +82,7 @@ export const BlueprintActionsMenu: React.FunctionComponent<
|
|||
|
||||
async function handleExportBlueprint(
|
||||
blueprintName: string,
|
||||
blueprint: BlueprintExportResponse,
|
||||
blueprint: BlueprintExportResponse
|
||||
) {
|
||||
const jsonData = JSON.stringify(blueprint, null, 2);
|
||||
const blob = new Blob([jsonData], { type: 'application/json' });
|
||||
|
|
|
|||
|
|
@ -3,20 +3,22 @@ import React from 'react';
|
|||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
Spinner,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
|
||||
import {
|
||||
selectSelectedBlueprintId,
|
||||
setBlueprintId,
|
||||
} from '../../store/BlueprintSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||
import { BlueprintItem } from '../../store/imageBuilderApi';
|
||||
import {
|
||||
BlueprintItem,
|
||||
useDeleteBlueprintMutation,
|
||||
} from '../../store/imageBuilderApi';
|
||||
|
||||
type blueprintProps = {
|
||||
blueprint: BlueprintItem;
|
||||
|
|
@ -26,45 +28,26 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
|
|||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { isLoading } = useDeleteBlueprintMutation({
|
||||
const [, { isLoading }] = useDeleteBlueprintMutation({
|
||||
fixedCacheKey: 'delete-blueprint',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
isClicked={blueprint.id === selectedBlueprintId}
|
||||
isSelected={blueprint.id === selectedBlueprintId}
|
||||
isSelectable
|
||||
data-testid={`blueprint-card`}
|
||||
isCompact
|
||||
isClickable
|
||||
onClick={() => dispatch(setBlueprintId(blueprint.id))}
|
||||
>
|
||||
<CardHeader
|
||||
data-testid={blueprint.id}
|
||||
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}>
|
||||
<CardHeader data-testid={blueprint.id}>
|
||||
<CardTitle>
|
||||
{isLoading && blueprint.id === selectedBlueprintId && (
|
||||
<Spinner size='md' />
|
||||
<Spinner size="md" />
|
||||
)}
|
||||
{
|
||||
// NOTE: This might be an issue with the pf6 truncate component.
|
||||
// Since we're not really using the popover, we can just
|
||||
// use vanilla js to truncate the string rather than use the
|
||||
// Truncate component. We can match the behaviour of the component
|
||||
// by also splitting on 24 characters.
|
||||
// https://github.com/patternfly/patternfly-react/issues/11964
|
||||
blueprint.name && blueprint.name.length > 24
|
||||
? blueprint.name.slice(0, 24) + '...'
|
||||
: blueprint.name
|
||||
}
|
||||
{blueprint.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>{blueprint.description}</CardBody>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import { DiffEditor } from '@monaco-editor/react';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalVariant,
|
||||
} from '@patternfly/react-core';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { Modal, ModalVariant } from '@patternfly/react-core/deprecated';
|
||||
|
||||
import { BuildImagesButton } from './BuildImagesButton';
|
||||
|
||||
|
|
@ -34,11 +28,11 @@ const BlueprintDiffModal = ({
|
|||
|
||||
const { data: baseBlueprint } = useGetBlueprintQuery(
|
||||
{ id: selectedBlueprintId as string, version: baseVersion || -1 },
|
||||
{ skip: !selectedBlueprintId || !baseVersion },
|
||||
{ skip: !selectedBlueprintId || !baseVersion }
|
||||
);
|
||||
const { data: blueprint } = useGetBlueprintQuery(
|
||||
{ id: selectedBlueprintId as string },
|
||||
{ skip: !selectedBlueprintId },
|
||||
{ skip: !selectedBlueprintId }
|
||||
);
|
||||
|
||||
if (!baseBlueprint || !blueprint) {
|
||||
|
|
@ -46,32 +40,32 @@ const BlueprintDiffModal = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal variant={ModalVariant.large} isOpen={isOpen} onClose={onClose}>
|
||||
<ModalHeader
|
||||
title={`Compare ${blueprintName || ''} versions`}
|
||||
titleIconVariant={'info'}
|
||||
/>
|
||||
<ModalBody>
|
||||
<DiffEditor
|
||||
height='90vh'
|
||||
language='json'
|
||||
original={JSON.stringify(baseBlueprint, undefined, 2)}
|
||||
modified={JSON.stringify(blueprint, undefined, 2)}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<BuildImagesButton key='build-button'>
|
||||
<Modal
|
||||
variant={ModalVariant.large}
|
||||
titleIconVariant={'info'}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={`Compare ${blueprintName || ''} versions`}
|
||||
actions={[
|
||||
<BuildImagesButton key="build-button">
|
||||
Synchronize images
|
||||
</BuildImagesButton>
|
||||
</BuildImagesButton>,
|
||||
<Button
|
||||
key='cancel-button'
|
||||
variant='link'
|
||||
type='button'
|
||||
key="cancel-button"
|
||||
variant="link"
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<DiffEditor
|
||||
height="90vh"
|
||||
language="json"
|
||||
original={JSON.stringify(baseBlueprint, undefined, 2)}
|
||||
modified={JSON.stringify(blueprint, undefined, 2)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/Me
|
|||
import { FilterIcon } from '@patternfly/react-icons';
|
||||
|
||||
import {
|
||||
versionFilterType,
|
||||
selectBlueprintVersionFilter,
|
||||
setBlueprintVersionFilter,
|
||||
versionFilterType,
|
||||
} from '../../store/BlueprintSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ const BlueprintVersionFilter: React.FC<blueprintVersionFilterProps> = ({
|
|||
|
||||
const onSelect = (
|
||||
_event: React.MouseEvent<Element, MouseEvent> | undefined,
|
||||
value: versionFilterType,
|
||||
value: versionFilterType
|
||||
) => {
|
||||
dispatch(setBlueprintVersionFilter(value));
|
||||
if (onFilterChange) onFilterChange();
|
||||
|
|
@ -58,10 +58,10 @@ const BlueprintVersionFilter: React.FC<blueprintVersionFilterProps> = ({
|
|||
shouldFocusToggleOnSelect
|
||||
>
|
||||
<DropdownList>
|
||||
<DropdownItem value={'all'} key='all'>
|
||||
<DropdownItem value={'all'} key="all">
|
||||
All versions
|
||||
</DropdownItem>
|
||||
<DropdownItem value={'latest'} key='newest'>
|
||||
<DropdownItem value={'latest'} key="newest">
|
||||
Newest
|
||||
</DropdownItem>
|
||||
</DropdownList>
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ const BlueprintsPagination = () => {
|
|||
page={currPage}
|
||||
onSetPage={onSetPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
widgetId='blueprints-pagination-bottom'
|
||||
data-testid='blueprints-pagination-bottom'
|
||||
widgetId="blueprints-pagination-bottom"
|
||||
data-testid="blueprints-pagination-bottom"
|
||||
isCompact
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
Bullseye,
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
||||
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
|
@ -25,10 +26,9 @@ import BlueprintsPagination from './BlueprintsPagination';
|
|||
|
||||
import {
|
||||
DEBOUNCED_SEARCH_WAIT_TIME,
|
||||
PAGINATION_LIMIT,
|
||||
PAGINATION_OFFSET,
|
||||
PAGINATION_LIMIT,
|
||||
} from '../../constants';
|
||||
import { useGetUser } from '../../Hooks';
|
||||
import { useGetBlueprintsQuery } from '../../store/backendApi';
|
||||
import {
|
||||
selectBlueprintSearchInput,
|
||||
|
|
@ -46,6 +46,7 @@ import {
|
|||
} from '../../store/imageBuilderApi';
|
||||
import { imageBuilderApi } from '../../store/service/enhancedImageBuilderApi';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useGetEnvironment } from '../../Utilities/useGetEnvironment';
|
||||
|
||||
type blueprintSearchProps = {
|
||||
blueprintsTotal: number;
|
||||
|
|
@ -60,8 +61,9 @@ type emptyBlueprintStateProps = {
|
|||
};
|
||||
|
||||
const BlueprintsSidebar = () => {
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
const { isFedoraEnv } = useGetEnvironment();
|
||||
const { analytics, auth } = useChrome();
|
||||
const { userData } = useGetUser(auth);
|
||||
|
||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
||||
|
|
@ -73,6 +75,13 @@ const BlueprintsSidebar = () => {
|
|||
offset: blueprintsOffset,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await auth?.getUser();
|
||||
setUserData(data);
|
||||
})();
|
||||
}, [auth]);
|
||||
|
||||
if (blueprintSearchInput) {
|
||||
searchParams.search = blueprintSearchInput;
|
||||
}
|
||||
|
|
@ -90,7 +99,7 @@ const BlueprintsSidebar = () => {
|
|||
if (isLoading) {
|
||||
return (
|
||||
<Bullseye>
|
||||
<Spinner size='xl' />
|
||||
<Spinner size="xl" />
|
||||
</Bullseye>
|
||||
);
|
||||
}
|
||||
|
|
@ -104,8 +113,8 @@ const BlueprintsSidebar = () => {
|
|||
<EmptyBlueprintState
|
||||
icon={PlusCircleIcon}
|
||||
action={<Link to={resolveRelPath('imagewizard')}>Add blueprint</Link>}
|
||||
titleText='No blueprints yet'
|
||||
bodyText='Add a blueprint and optionally build related images.'
|
||||
titleText="No blueprints yet"
|
||||
bodyText="Add a blueprint and optionally build related images."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -115,8 +124,8 @@ const BlueprintsSidebar = () => {
|
|||
dispatch(setBlueprintId(undefined));
|
||||
};
|
||||
|
||||
if (!process.env.IS_ON_PREMISE) {
|
||||
const orgId = userData?.identity.internal?.org_id;
|
||||
if (!process.env.IS_ON_PREMISE && !isFedoraEnv) {
|
||||
const orgId = userData?.identity?.internal?.org_id;
|
||||
|
||||
analytics.group(orgId, {
|
||||
imagebuilder_blueprint_count: blueprintsData?.meta.count,
|
||||
|
|
@ -137,7 +146,7 @@ const BlueprintsSidebar = () => {
|
|||
<Flex justifyContent={{ default: 'justifyContentCenter' }}>
|
||||
<FlexItem>
|
||||
<Button
|
||||
variant='link'
|
||||
variant="link"
|
||||
isDisabled={!selectedBlueprintId}
|
||||
onClick={handleClickViewAll}
|
||||
>
|
||||
|
|
@ -153,14 +162,14 @@ const BlueprintsSidebar = () => {
|
|||
icon={SearchIcon}
|
||||
action={
|
||||
<Button
|
||||
variant='link'
|
||||
variant="link"
|
||||
onClick={() => dispatch(setBlueprintSearchInput(undefined))}
|
||||
>
|
||||
Clear all filters
|
||||
</Button>
|
||||
}
|
||||
titleText='No blueprints found'
|
||||
bodyText='No blueprints match your search criteria. Try a different search.'
|
||||
titleText="No blueprints found"
|
||||
bodyText="No blueprints match your search criteria. Try a different search."
|
||||
/>
|
||||
)}
|
||||
{blueprintsTotal > 0 &&
|
||||
|
|
@ -184,7 +193,7 @@ const BlueprintSearch = ({ blueprintsTotal }: blueprintSearchProps) => {
|
|||
dispatch(imageBuilderApi.util.invalidateTags([{ type: 'Blueprints' }]));
|
||||
dispatch(setBlueprintSearchInput(filter.length > 0 ? filter : undefined));
|
||||
}, DEBOUNCED_SEARCH_WAIT_TIME),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -202,7 +211,7 @@ const BlueprintSearch = ({ blueprintsTotal }: blueprintSearchProps) => {
|
|||
return (
|
||||
<SearchInput
|
||||
value={blueprintSearchInput || ''}
|
||||
placeholder='Search by name or description'
|
||||
placeholder="Search by name or description"
|
||||
onChange={(_event, value) => onChange(value)}
|
||||
onClear={() => onChange('')}
|
||||
resultsCount={`${blueprintsTotal} blueprints`}
|
||||
|
|
@ -216,7 +225,7 @@ const EmptyBlueprintState = ({
|
|||
icon,
|
||||
action,
|
||||
}: emptyBlueprintStateProps) => (
|
||||
<EmptyState headingLevel='h4' icon={icon} titleText={titleText} variant='sm'>
|
||||
<EmptyState headingLevel="h4" icon={icon} titleText={titleText} variant="sm">
|
||||
<EmptyStateBody>{bodyText}</EmptyStateBody>
|
||||
<EmptyStateFooter>
|
||||
<EmptyStateActions>{action}</EmptyStateActions>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Dropdown,
|
||||
Flex,
|
||||
FlexItem,
|
||||
MenuToggle,
|
||||
Menu,
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
MenuToggle,
|
||||
MenuToggleAction,
|
||||
MenuItem,
|
||||
Flex,
|
||||
FlexItem,
|
||||
Spinner,
|
||||
MenuToggleAction,
|
||||
ButtonProps,
|
||||
Button,
|
||||
} from '@patternfly/react-core';
|
||||
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
|
||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
|
||||
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
|
||||
import {
|
||||
useComposeBPWithNotification as useComposeBlueprintMutation,
|
||||
useGetUser,
|
||||
} from '../../Hooks';
|
||||
import { useGetBlueprintQuery } from '../../store/backendApi';
|
||||
useGetBlueprintQuery,
|
||||
useComposeBlueprintMutation,
|
||||
} from '../../store/backendApi';
|
||||
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
|
||||
import { useAppSelector } from '../../store/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||
import { ImageTypes } from '../../store/imageBuilderApi';
|
||||
|
||||
type BuildImagesButtonPropTypes = {
|
||||
|
|
@ -36,27 +37,44 @@ type BuildImagesButtonPropTypes = {
|
|||
export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||
const [deselectedTargets, setDeselectedTargets] = useState<ImageTypes[]>([]);
|
||||
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
||||
const [buildBlueprint, { isLoading: imageBuildLoading }] =
|
||||
useComposeBlueprintMutation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { analytics, auth } = useChrome();
|
||||
const { userData } = useGetUser(auth);
|
||||
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await auth?.getUser();
|
||||
setUserData(data);
|
||||
})();
|
||||
}, [auth]);
|
||||
|
||||
const onBuildHandler = async () => {
|
||||
if (selectedBlueprintId) {
|
||||
await buildBlueprint({
|
||||
id: selectedBlueprintId,
|
||||
body: {
|
||||
image_types: blueprintImageType?.filter(
|
||||
(target) => !deselectedTargets.includes(target),
|
||||
),
|
||||
},
|
||||
});
|
||||
if (!process.env.IS_ON_PREMISE) {
|
||||
try {
|
||||
await buildBlueprint({
|
||||
id: selectedBlueprintId,
|
||||
body: {
|
||||
image_types: blueprintImageType?.filter(
|
||||
(target) => !deselectedTargets.includes(target)
|
||||
),
|
||||
},
|
||||
});
|
||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Requested`, {
|
||||
module: AMPLITUDE_MODULE_NAME,
|
||||
trigger: 'synchronize images',
|
||||
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);
|
||||
};
|
||||
const { data: blueprintDetails } = useGetBlueprintQuery(
|
||||
selectedBlueprintId ? { id: selectedBlueprintId } : skipToken,
|
||||
selectedBlueprintId ? { id: selectedBlueprintId } : skipToken
|
||||
);
|
||||
const blueprintImageType = blueprintDetails?.image_requests.map(
|
||||
(image) => image.image_type,
|
||||
(image) => image.image_type
|
||||
);
|
||||
|
||||
const onSelect = (
|
||||
_event: React.MouseEvent<Element, MouseEvent>,
|
||||
itemId: number,
|
||||
itemId: number
|
||||
) => {
|
||||
const imageType = blueprintImageType?.[itemId];
|
||||
|
||||
if (imageType && deselectedTargets.includes(imageType)) {
|
||||
setDeselectedTargets(
|
||||
deselectedTargets.filter((target) => target !== imageType),
|
||||
deselectedTargets.filter((target) => target !== imageType)
|
||||
);
|
||||
} else if (imageType) {
|
||||
setDeselectedTargets([...deselectedTargets, imageType]);
|
||||
|
|
@ -92,17 +110,17 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
|||
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
|
||||
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
|
||||
<MenuToggle
|
||||
variant='primary'
|
||||
data-testid='blueprint-build-image-menu'
|
||||
variant="primary"
|
||||
data-testid="blueprint-build-image-menu"
|
||||
ref={toggleRef}
|
||||
onClick={onToggleClick}
|
||||
isExpanded={isOpen}
|
||||
splitButtonItems={[
|
||||
<MenuToggleAction
|
||||
data-testid='blueprint-build-image-menu-option'
|
||||
key='split-action'
|
||||
data-testid="blueprint-build-image-menu-option"
|
||||
key="split-action"
|
||||
onClick={onBuildHandler}
|
||||
id='wizard-build-image-btn'
|
||||
id="wizard-build-image-btn"
|
||||
isDisabled={
|
||||
!selectedBlueprintId ||
|
||||
deselectedTargets.length === blueprintImageType?.length
|
||||
|
|
@ -118,7 +136,7 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
|||
} as React.CSSProperties
|
||||
}
|
||||
isInline
|
||||
size='md'
|
||||
size="md"
|
||||
/>
|
||||
</FlexItem>
|
||||
)}
|
||||
|
|
@ -162,7 +180,7 @@ export const BuildImagesButtonEmptyState = ({
|
|||
children,
|
||||
}: BuildImagesButtonEmptyStatePropTypes) => {
|
||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
||||
const [buildBlueprint, { isLoading: imageBuildLoading }] =
|
||||
useComposeBlueprintMutation();
|
||||
const onBuildHandler = async () => {
|
||||
if (selectedBlueprintId) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalVariant,
|
||||
} from '@patternfly/react-core';
|
||||
import { ActionGroup, Button } from '@patternfly/react-core';
|
||||
import { Modal, ModalVariant } from '@patternfly/react-core/deprecated';
|
||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||
|
||||
import {
|
||||
AMPLITUDE_MODULE_NAME,
|
||||
|
|
@ -16,10 +11,10 @@ import {
|
|||
PAGINATION_OFFSET,
|
||||
} from '../../constants';
|
||||
import {
|
||||
useDeleteBPWithNotification as useDeleteBlueprintMutation,
|
||||
useGetUser,
|
||||
} from '../../Hooks';
|
||||
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
||||
backendApi,
|
||||
useDeleteBlueprintMutation,
|
||||
useGetBlueprintsQuery,
|
||||
} from '../../store/backendApi';
|
||||
import {
|
||||
selectBlueprintSearchInput,
|
||||
selectLimit,
|
||||
|
|
@ -44,7 +39,14 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
|||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||
const dispatch = useAppDispatch();
|
||||
const { analytics, auth } = useChrome();
|
||||
const { userData } = useGetUser(auth);
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await auth?.getUser();
|
||||
setUserData(data);
|
||||
})();
|
||||
}, [auth]);
|
||||
|
||||
const searchParams: GetBlueprintsApiArg = {
|
||||
limit: blueprintsLimit,
|
||||
|
|
@ -59,21 +61,19 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
|||
selectFromResult: ({ data }) => ({
|
||||
blueprintName: data?.data.find(
|
||||
(blueprint: { id: string | undefined }) =>
|
||||
blueprint.id === selectedBlueprintId,
|
||||
blueprint.id === selectedBlueprintId
|
||||
)?.name,
|
||||
}),
|
||||
});
|
||||
const { trigger: deleteBlueprint } = useDeleteBlueprintMutation({
|
||||
const [deleteBlueprint] = useDeleteBlueprintMutation({
|
||||
fixedCacheKey: 'delete-blueprint',
|
||||
});
|
||||
const handleDelete = async () => {
|
||||
if (selectedBlueprintId) {
|
||||
if (!process.env.IS_ON_PREMISE) {
|
||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Deleted`, {
|
||||
module: AMPLITUDE_MODULE_NAME,
|
||||
account_id: userData?.identity.internal?.account_id || 'Not found',
|
||||
});
|
||||
}
|
||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Deleted`, {
|
||||
module: AMPLITUDE_MODULE_NAME,
|
||||
account_id: userData?.identity.internal?.account_id || 'Not found',
|
||||
});
|
||||
setShowDeleteModal(false);
|
||||
await deleteBlueprint({ id: selectedBlueprintId });
|
||||
dispatch(setBlueprintId(undefined));
|
||||
|
|
@ -84,20 +84,22 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
|||
setShowDeleteModal(false);
|
||||
};
|
||||
return (
|
||||
<Modal variant={ModalVariant.small} isOpen={isOpen} onClose={onDeleteClose}>
|
||||
<ModalHeader title={'Delete blueprint?'} titleIconVariant='warning' />
|
||||
<ModalBody>
|
||||
All versions of {blueprintName} and its associated images will be
|
||||
deleted.
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='danger' type='button' onClick={handleDelete}>
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
titleIconVariant="warning"
|
||||
isOpen={isOpen}
|
||||
onClose={onDeleteClose}
|
||||
title={'Delete blueprint?'}
|
||||
description={`All versions of ${blueprintName} and its associated images will be deleted.`}
|
||||
>
|
||||
<ActionGroup>
|
||||
<Button variant="danger" type="button" onClick={handleDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant='link' type='button' onClick={onDeleteClose}>
|
||||
<Button variant="link" type="button" onClick={onDeleteClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ActionGroup>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const EditBlueprintButton = () => {
|
|||
onClick={() =>
|
||||
navigate(resolveRelPath(`imagewizard/${selectedBlueprintId}`))
|
||||
}
|
||||
variant='secondary'
|
||||
variant="secondary"
|
||||
>
|
||||
Edit blueprint
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
|
||||
import { parse } from '@ltd/j-toml';
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
Checkbox,
|
||||
FileUpload,
|
||||
|
|
@ -10,16 +11,12 @@ import {
|
|||
FormHelperText,
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalVariant,
|
||||
Popover,
|
||||
} from '@patternfly/react-core';
|
||||
import { Modal, ModalVariant } from '@patternfly/react-core/deprecated';
|
||||
import { DropEvent } from '@patternfly/react-core/dist/esm/helpers';
|
||||
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 { mapOnPremToHosted } from './helpers/onPremToHostedBlueprintMapper';
|
||||
|
|
@ -29,6 +26,7 @@ import {
|
|||
ApiRepositoryRequest,
|
||||
useBulkImportRepositoriesMutation,
|
||||
} from '../../store/contentSourcesApi';
|
||||
import { useAppDispatch } from '../../store/hooks';
|
||||
import {
|
||||
BlueprintExportResponse,
|
||||
BlueprintItem,
|
||||
|
|
@ -66,24 +64,24 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
const [isRejected, setIsRejected] = React.useState(false);
|
||||
const [isOnPrem, setIsOnPrem] = React.useState(false);
|
||||
const [isCheckedImportRepos, setIsCheckedImportRepos] = React.useState(true);
|
||||
const addNotification = useAddNotification();
|
||||
const dispatch = useAppDispatch();
|
||||
const [importRepositories] = useBulkImportRepositoriesMutation();
|
||||
|
||||
const handleFileInputChange = (
|
||||
_event: React.ChangeEvent<HTMLInputElement> | React.DragEvent<HTMLElement>,
|
||||
file: File,
|
||||
file: File
|
||||
) => {
|
||||
setFileContent('');
|
||||
setFilename(file.name);
|
||||
};
|
||||
|
||||
async function handleRepositoryImport(
|
||||
blueprintExportedResponse: BlueprintExportResponse,
|
||||
blueprintExportedResponse: BlueprintExportResponse
|
||||
): Promise<CustomRepository[] | undefined> {
|
||||
if (isCheckedImportRepos && blueprintExportedResponse.content_sources) {
|
||||
const customRepositories: ApiRepositoryRequest[] =
|
||||
blueprintExportedResponse.content_sources.map(
|
||||
(item) => item as ApiRepositoryRequest,
|
||||
(item) => item as ApiRepositoryRequest
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
@ -98,34 +96,40 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
repository as ApiRepositoryImportResponseRead;
|
||||
if (contentSourcesRepo.uuid) {
|
||||
newCustomRepos.push(
|
||||
...mapToCustomRepositories(contentSourcesRepo),
|
||||
...mapToCustomRepositories(contentSourcesRepo)
|
||||
);
|
||||
}
|
||||
if (repository.warnings?.length === 0 && repository.url) {
|
||||
importedRepositoryNames.push(repository.url);
|
||||
return;
|
||||
}
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'Failed to import custom repositories',
|
||||
description: JSON.stringify(repository.warnings),
|
||||
});
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'Failed to import custom repositories',
|
||||
description: JSON.stringify(repository.warnings),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (importedRepositoryNames.length !== 0) {
|
||||
addNotification({
|
||||
variant: 'info',
|
||||
title: 'Successfully imported custom repositories',
|
||||
description: importedRepositoryNames.join(', '),
|
||||
});
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'info',
|
||||
title: 'Successfully imported custom repositories',
|
||||
description: importedRepositoryNames.join(', '),
|
||||
})
|
||||
);
|
||||
}
|
||||
return newCustomRepos;
|
||||
}
|
||||
} catch {
|
||||
addNotification({
|
||||
variant: 'danger',
|
||||
title: 'Custom repositories import failed',
|
||||
});
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'danger',
|
||||
title: 'Custom repositories import failed',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -139,11 +143,11 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
if (isToml) {
|
||||
const tomlBlueprint = parse(fileContent);
|
||||
const blueprintFromFile = mapOnPremToHosted(
|
||||
tomlBlueprint as BlueprintItem,
|
||||
tomlBlueprint as BlueprintItem
|
||||
);
|
||||
const importBlueprintState = mapExportRequestToState(
|
||||
blueprintFromFile,
|
||||
[],
|
||||
[]
|
||||
);
|
||||
setIsOnPrem(true);
|
||||
setImportedBlueprint(importBlueprintState);
|
||||
|
|
@ -155,8 +159,9 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
blueprintFromFile.content_sources &&
|
||||
blueprintFromFile.content_sources.length > 0
|
||||
) {
|
||||
const imported =
|
||||
await handleRepositoryImport(blueprintFromFile);
|
||||
const imported = await handleRepositoryImport(
|
||||
blueprintFromFile
|
||||
);
|
||||
customRepos = imported ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +179,7 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
undefined;
|
||||
const importBlueprintState = mapExportRequestToState(
|
||||
blueprintExportedResponse,
|
||||
blueprintFromFile.image_requests || [],
|
||||
blueprintFromFile.image_requests || []
|
||||
);
|
||||
|
||||
setIsOnPrem(false);
|
||||
|
|
@ -184,7 +189,7 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
mapOnPremToHosted(blueprintFromFile);
|
||||
const importBlueprintState = mapExportRequestToState(
|
||||
blueprintFromFileMapped,
|
||||
[],
|
||||
[]
|
||||
);
|
||||
setIsOnPrem(true);
|
||||
setImportedBlueprint(importBlueprintState);
|
||||
|
|
@ -192,11 +197,13 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
}
|
||||
} catch (error) {
|
||||
setIsInvalidFormat(true);
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'File is not a valid blueprint',
|
||||
description: error?.data?.error?.message,
|
||||
});
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'File is not a valid blueprint',
|
||||
description: error?.data?.error?.message,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
parseAndImport();
|
||||
|
|
@ -241,98 +248,94 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
<Modal
|
||||
variant={ModalVariant.medium}
|
||||
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
|
||||
icon={<HelpIcon />}
|
||||
variant="plain"
|
||||
aria-label="About import"
|
||||
className="pf-v6-u-pl-sm"
|
||||
isInline
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
onClose={onImportClose}
|
||||
>
|
||||
<ModalHeader
|
||||
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
|
||||
icon={<HelpIcon />}
|
||||
variant='plain'
|
||||
aria-label='About import'
|
||||
className='pf-v6-u-pl-sm'
|
||||
isInline
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup fieldId='checkbox-import-custom-repositories'>
|
||||
<Checkbox
|
||||
label='Import missing custom repositories after file upload.'
|
||||
isChecked={isCheckedImportRepos}
|
||||
onChange={() => setIsCheckedImportRepos((prev) => !prev)}
|
||||
aria-label='Import Custom Repositories checkbox'
|
||||
id='checkbox-import-custom-repositories'
|
||||
name='Import Repositories'
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup fieldId='import-blueprint-file-upload'>
|
||||
<FileUpload
|
||||
id='import-blueprint-file-upload'
|
||||
type='text'
|
||||
value={fileContent}
|
||||
filename={filename}
|
||||
filenamePlaceholder='Drag and drop a file or upload one'
|
||||
onFileInputChange={handleFileInputChange}
|
||||
onDataChange={handleDataChange}
|
||||
onReadStarted={handleFileReadStarted}
|
||||
onReadFinished={handleFileReadFinished}
|
||||
onClearClick={handleClear}
|
||||
isLoading={isLoading}
|
||||
isReadOnly={true}
|
||||
browseButtonText='Upload'
|
||||
dropzoneProps={{
|
||||
accept: { 'text/json': ['.json'], 'text/plain': ['.toml'] },
|
||||
maxSize: 512000,
|
||||
onDropRejected: handleFileRejected,
|
||||
}}
|
||||
validated={isRejected || isInvalidFormat ? 'error' : 'default'}
|
||||
/>
|
||||
<FormHelperText>
|
||||
<HelperText>
|
||||
<HelperTextItem variant={variantSwitch()}>
|
||||
{isRejected
|
||||
? 'Must be a valid Blueprint JSON/TOML file no larger than 512 KB'
|
||||
: isInvalidFormat
|
||||
? 'Not compatible with the blueprints format.'
|
||||
: 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>
|
||||
<Form>
|
||||
<FormGroup fieldId="checkbox-import-custom-repositories">
|
||||
<Checkbox
|
||||
label="Import missing custom repositories after file upload."
|
||||
isChecked={isCheckedImportRepos}
|
||||
onChange={() => setIsCheckedImportRepos((prev) => !prev)}
|
||||
aria-label="Import Custom Repositories checkbox"
|
||||
id="checkbox-import-custom-repositories"
|
||||
name="Import Repositories"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup fieldId="import-blueprint-file-upload">
|
||||
<FileUpload
|
||||
id="import-blueprint-file-upload"
|
||||
type="text"
|
||||
value={fileContent}
|
||||
filename={filename}
|
||||
filenamePlaceholder="Drag and drop a file or upload one"
|
||||
onFileInputChange={handleFileInputChange}
|
||||
onDataChange={handleDataChange}
|
||||
onReadStarted={handleFileReadStarted}
|
||||
onReadFinished={handleFileReadFinished}
|
||||
onClearClick={handleClear}
|
||||
isLoading={isLoading}
|
||||
isReadOnly={true}
|
||||
browseButtonText="Upload"
|
||||
dropzoneProps={{
|
||||
accept: { 'text/json': ['.json'], 'text/plain': ['.toml'] },
|
||||
maxSize: 512000,
|
||||
onDropRejected: handleFileRejected,
|
||||
}}
|
||||
validated={isRejected || isInvalidFormat ? 'error' : 'default'}
|
||||
/>
|
||||
<FormHelperText>
|
||||
<HelperText>
|
||||
<HelperTextItem variant={variantSwitch()}>
|
||||
{isRejected
|
||||
? 'Must be a valid Blueprint JSON/TOML file no larger than 512 KB'
|
||||
: isInvalidFormat
|
||||
? 'Not compatible with the blueprints format.'
|
||||
: isOnPrem
|
||||
? 'Importing on-premises blueprints is currently in beta. Results may vary.'
|
||||
: 'Upload your blueprint file. Supported formats: JSON, TOML.'}
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
type="button"
|
||||
isDisabled={isRejected || isInvalidFormat || !fileContent}
|
||||
onClick={() =>
|
||||
navigate(resolveRelPath(`imagewizard/import`), {
|
||||
state: { blueprint: importedBlueprint },
|
||||
})
|
||||
}
|
||||
data-testid="import-blueprint-finish"
|
||||
>
|
||||
Review and finish
|
||||
</Button>
|
||||
<Button variant="link" type="button" onClick={onImportClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -101,13 +101,13 @@ export type SshKeyOnPrem = {
|
|||
};
|
||||
|
||||
export const mapOnPremToHosted = (
|
||||
blueprint: BlueprintOnPrem,
|
||||
blueprint: BlueprintOnPrem
|
||||
): BlueprintExportResponse => {
|
||||
const users = blueprint.customizations?.user?.map((u) => ({
|
||||
name: u.name,
|
||||
ssh_key: u.key,
|
||||
groups: u.groups,
|
||||
isAdministrator: u.groups.includes('wheel') || false,
|
||||
isAdministrator: u.groups?.includes('wheel') || false,
|
||||
}));
|
||||
const user_keys = blueprint.customizations?.sshkey?.map((k) => ({
|
||||
name: k.user,
|
||||
|
|
@ -132,7 +132,7 @@ export const mapOnPremToHosted = (
|
|||
({ baseurls, ...fs }) => ({
|
||||
baseurl: baseurls,
|
||||
...fs,
|
||||
}),
|
||||
})
|
||||
),
|
||||
packages:
|
||||
packages !== undefined || groups !== undefined
|
||||
|
|
@ -147,7 +147,7 @@ export const mapOnPremToHosted = (
|
|||
({ minsize, ...fs }) => ({
|
||||
min_size: minsize,
|
||||
...fs,
|
||||
}),
|
||||
})
|
||||
),
|
||||
fips:
|
||||
blueprint.customizations?.fips !== undefined
|
||||
|
|
@ -189,14 +189,14 @@ export const mapOnPremToHosted = (
|
|||
};
|
||||
|
||||
export const mapHostedToOnPrem = (
|
||||
blueprint: CreateBlueprintRequest,
|
||||
blueprint: CreateBlueprintRequest
|
||||
): CloudApiBlueprint => {
|
||||
const result: CloudApiBlueprint = {
|
||||
name: blueprint.name,
|
||||
customizations: {},
|
||||
};
|
||||
|
||||
if (blueprint.customizations.packages) {
|
||||
if (blueprint.customizations?.packages) {
|
||||
result.packages = blueprint.customizations.packages.map((pkg) => {
|
||||
return {
|
||||
name: pkg,
|
||||
|
|
@ -205,30 +205,30 @@ export const mapHostedToOnPrem = (
|
|||
});
|
||||
}
|
||||
|
||||
if (blueprint.customizations.containers) {
|
||||
if (blueprint.customizations?.containers) {
|
||||
result.containers = blueprint.customizations.containers;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.directories) {
|
||||
if (blueprint.customizations?.directories) {
|
||||
result.customizations!.directories = blueprint.customizations.directories;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.files) {
|
||||
if (blueprint.customizations?.files) {
|
||||
result.customizations!.files = blueprint.customizations.files;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.filesystem) {
|
||||
if (blueprint.customizations?.filesystem) {
|
||||
result.customizations!.filesystem = blueprint.customizations.filesystem.map(
|
||||
(fs) => {
|
||||
return {
|
||||
mountpoint: fs.mountpoint,
|
||||
minsize: fs.min_size,
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (blueprint.customizations.users) {
|
||||
if (blueprint.customizations?.users) {
|
||||
result.customizations!.user = blueprint.customizations.users.map((u) => {
|
||||
return {
|
||||
name: u.name,
|
||||
|
|
@ -239,54 +239,54 @@ export const mapHostedToOnPrem = (
|
|||
});
|
||||
}
|
||||
|
||||
if (blueprint.customizations.services) {
|
||||
if (blueprint.customizations?.services) {
|
||||
result.customizations!.services = blueprint.customizations.services;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.hostname) {
|
||||
if (blueprint.customizations?.hostname) {
|
||||
result.customizations!.hostname = blueprint.customizations.hostname;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.kernel) {
|
||||
if (blueprint.customizations?.kernel) {
|
||||
result.customizations!.kernel = blueprint.customizations.kernel;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.timezone) {
|
||||
if (blueprint.customizations?.timezone) {
|
||||
result.customizations!.timezone = blueprint.customizations.timezone;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.locale) {
|
||||
if (blueprint.customizations?.locale) {
|
||||
result.customizations!.locale = blueprint.customizations.locale;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.firewall) {
|
||||
if (blueprint.customizations?.firewall) {
|
||||
result.customizations!.firewall = blueprint.customizations.firewall;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.installation_device) {
|
||||
if (blueprint.customizations?.installation_device) {
|
||||
result.customizations!.installation_device =
|
||||
blueprint.customizations.installation_device;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.fdo) {
|
||||
if (blueprint.customizations?.fdo) {
|
||||
result.customizations!.fdo = blueprint.customizations.fdo;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.ignition) {
|
||||
if (blueprint.customizations?.ignition) {
|
||||
result.customizations!.ignition = blueprint.customizations.ignition;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.partitioning_mode) {
|
||||
if (blueprint.customizations?.partitioning_mode) {
|
||||
result.customizations!.partitioning_mode =
|
||||
blueprint.customizations.partitioning_mode;
|
||||
}
|
||||
|
||||
if (blueprint.customizations.fips) {
|
||||
if (blueprint.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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
|
@ -13,7 +13,7 @@ import cockpit from 'cockpit';
|
|||
export const NotReady = ({ enabled }: { enabled: boolean }) => {
|
||||
return (
|
||||
<EmptyState
|
||||
headingLevel='h4'
|
||||
headingLevel="h4"
|
||||
icon={CubesIcon}
|
||||
titleText={`OSBuild Composer is not ${enabled ? 'started' : 'enabled'}`}
|
||||
variant={EmptyStateVariant.xl}
|
||||
|
|
@ -21,7 +21,7 @@ export const NotReady = ({ enabled }: { enabled: boolean }) => {
|
|||
<EmptyStateFooter>
|
||||
<EmptyStateActions>
|
||||
<Button
|
||||
variant='primary'
|
||||
variant="primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
cockpit
|
||||
|
|
@ -30,7 +30,7 @@ export const NotReady = ({ enabled }: { enabled: boolean }) => {
|
|||
{
|
||||
superuser: 'require',
|
||||
err: 'message',
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(() => window.location.reload());
|
||||
}}
|
||||
|
|
@ -40,12 +40,12 @@ export const NotReady = ({ enabled }: { enabled: boolean }) => {
|
|||
</EmptyStateActions>
|
||||
<EmptyStateActions>
|
||||
<Button
|
||||
variant='link'
|
||||
variant="link"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
cockpit.jump(
|
||||
'/system/services#/osbuild-composer.socket',
|
||||
cockpit.transport.host,
|
||||
cockpit.transport.host
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import { LockIcon } from '@patternfly/react-icons';
|
|||
export const RequireAdmin = () => {
|
||||
return (
|
||||
<EmptyState
|
||||
headingLevel='h4'
|
||||
headingLevel="h4"
|
||||
icon={LockIcon}
|
||||
titleText='Access is limited.'
|
||||
titleText="Access is limited."
|
||||
variant={EmptyStateVariant.xl}
|
||||
>
|
||||
<EmptyStateBody>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,27 @@
|
|||
--pf-c-form__group-label--PaddingBottom: var(--pf-v6-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-v6-global--Color--100);
|
||||
--pf-c-tile--before--BorderWidth: var(--pf-v6-global--BorderWidth--sm);
|
||||
--pf-c-tile--before--BorderColor: var(--pf-v6-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 {
|
||||
width: 3.5em;
|
||||
height: 3.5em;
|
||||
|
|
@ -53,8 +74,3 @@ div.pf-v6-c-alert.pf-m-inline.pf-m-plain.pf-m-warning {
|
|||
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,22 @@ import React, { useEffect, useState } from 'react';
|
|||
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
PageSection,
|
||||
PageSectionTypes,
|
||||
useWizardContext,
|
||||
Wizard,
|
||||
WizardFooterWrapper,
|
||||
WizardNavItem,
|
||||
WizardStep,
|
||||
useWizardContext,
|
||||
PageSection,
|
||||
PageSectionTypes,
|
||||
Flex,
|
||||
} from '@patternfly/react-core';
|
||||
import { WizardStepType } from '@patternfly/react-core/dist/esm/components/Wizard';
|
||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import AAPStep from './steps/AAP';
|
||||
import DetailsStep from './steps/Details';
|
||||
import FileSystemStep from './steps/FileSystem';
|
||||
import { FileSystemContext } from './steps/FileSystem/components/FileSystemTable';
|
||||
import { FileSystemContext } from './steps/FileSystem/FileSystemTable';
|
||||
import FirewallStep from './steps/Firewall';
|
||||
import FirstBootStep from './steps/FirstBoot';
|
||||
import HostnameStep from './steps/Hostname';
|
||||
|
|
@ -39,59 +38,60 @@ import Gcp from './steps/TargetEnvironment/Gcp';
|
|||
import TimezoneStep from './steps/Timezone';
|
||||
import UsersStep from './steps/Users';
|
||||
import { getHostArch, getHostDistro } from './utilities/getHostInfo';
|
||||
import { useHasSpecificTargetOnly } from './utilities/hasSpecificTargetOnly';
|
||||
import {
|
||||
useAAPValidation,
|
||||
useDetailsValidation,
|
||||
useFilesystemValidation,
|
||||
useFirewallValidation,
|
||||
useSnapshotValidation,
|
||||
useFirstBootValidation,
|
||||
useDetailsValidation,
|
||||
useRegistrationValidation,
|
||||
useHostnameValidation,
|
||||
useKernelValidation,
|
||||
useLocaleValidation,
|
||||
useRegistrationValidation,
|
||||
useServicesValidation,
|
||||
useSnapshotValidation,
|
||||
useTimezoneValidation,
|
||||
useUsersValidation,
|
||||
useTimezoneValidation,
|
||||
useFirewallValidation,
|
||||
useServicesValidation,
|
||||
useLocaleValidation,
|
||||
} from './utilities/useValidation';
|
||||
import {
|
||||
isAwsAccountIdValid,
|
||||
isAzureResourceGroupValid,
|
||||
isAzureSubscriptionIdValid,
|
||||
isAzureTenantGUIDValid,
|
||||
isAzureSubscriptionIdValid,
|
||||
isAzureResourceGroupValid,
|
||||
isGcpEmailValid,
|
||||
} from './validators';
|
||||
|
||||
import {
|
||||
AARCH64,
|
||||
AMPLITUDE_MODULE_NAME,
|
||||
RHEL_10,
|
||||
RHEL_8,
|
||||
RHEL_9,
|
||||
RHEL_10_BETA,
|
||||
RHEL_10,
|
||||
AARCH64,
|
||||
CENTOS_9,
|
||||
AMPLITUDE_MODULE_NAME,
|
||||
} from '../../constants';
|
||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||
import './CreateImageWizard.scss';
|
||||
import {
|
||||
addImageType,
|
||||
changeArchitecture,
|
||||
changeAwsShareMethod,
|
||||
changeDistribution,
|
||||
changeArchitecture,
|
||||
initializeWizard,
|
||||
selectAwsAccountId,
|
||||
selectAwsShareMethod,
|
||||
selectAwsSourceId,
|
||||
selectAzureResourceGroup,
|
||||
selectAzureShareMethod,
|
||||
selectAzureSource,
|
||||
selectAzureSubscriptionId,
|
||||
selectAzureTenantId,
|
||||
selectDistribution,
|
||||
selectGcpEmail,
|
||||
selectGcpShareMethod,
|
||||
selectImageTypes,
|
||||
addImageType,
|
||||
changeRegistrationType,
|
||||
} from '../../store/wizardSlice';
|
||||
import isRhel from '../../Utilities/isRhel';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useFlag } from '../../Utilities/useGetEnvironment';
|
||||
import { useFlag, useGetEnvironment } from '../../Utilities/useGetEnvironment';
|
||||
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
|
||||
|
||||
type CustomWizardFooterPropType = {
|
||||
|
|
@ -118,7 +118,7 @@ export const CustomWizardFooter = ({
|
|||
<WizardFooterWrapper>
|
||||
<Flex columnGap={{ default: 'columnGapSm' }}>
|
||||
<Button
|
||||
variant='primary'
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (!process.env.IS_ON_PREMISE) {
|
||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
||||
|
|
@ -134,7 +134,7 @@ export const CustomWizardFooter = ({
|
|||
Next
|
||||
</Button>
|
||||
<Button
|
||||
variant='secondary'
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
if (!process.env.IS_ON_PREMISE) {
|
||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
||||
|
|
@ -151,7 +151,7 @@ export const CustomWizardFooter = ({
|
|||
</Button>
|
||||
{optional && (
|
||||
<Button
|
||||
variant='tertiary'
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
if (!process.env.IS_ON_PREMISE) {
|
||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
||||
|
|
@ -168,7 +168,7 @@ export const CustomWizardFooter = ({
|
|||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='link'
|
||||
variant="link"
|
||||
onClick={() => {
|
||||
if (!process.env.IS_ON_PREMISE) {
|
||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
|
||||
|
|
@ -196,19 +196,23 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { isFedoraEnv } = useGetEnvironment();
|
||||
// Feature flags
|
||||
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
|
||||
useEffect(() => {
|
||||
dispatch(initializeWizard());
|
||||
if (isFedoraEnv) {
|
||||
dispatch(changeDistribution(CENTOS_9));
|
||||
dispatch(changeRegistrationType('register-later'));
|
||||
}
|
||||
if (searchParams.get('release') === 'rhel8') {
|
||||
dispatch(changeDistribution(RHEL_8));
|
||||
}
|
||||
if (searchParams.get('release') === 'rhel9') {
|
||||
dispatch(changeDistribution(RHEL_9));
|
||||
if (searchParams.get('release') === 'rhel10beta') {
|
||||
dispatch(changeDistribution(RHEL_10_BETA));
|
||||
}
|
||||
if (searchParams.get('release') === 'rhel10') {
|
||||
dispatch(changeDistribution(RHEL_10));
|
||||
|
|
@ -233,10 +237,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
dispatch(changeArchitecture(arch));
|
||||
};
|
||||
|
||||
if (process.env.IS_ON_PREMISE) {
|
||||
dispatch(changeAwsShareMethod('manual'));
|
||||
}
|
||||
|
||||
if (process.env.IS_ON_PREMISE && !isEdit) {
|
||||
if (!searchParams.get('release')) {
|
||||
initializeHostDistro();
|
||||
|
|
@ -264,9 +264,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
const gcpShareMethod = useAppSelector(selectGcpShareMethod);
|
||||
const gcpEmail = useAppSelector(selectGcpEmail);
|
||||
// AZURE
|
||||
const azureShareMethod = useAppSelector(selectAzureShareMethod);
|
||||
const azureTenantId = useAppSelector(selectAzureTenantId);
|
||||
const azureSubscriptionId = useAppSelector(selectAzureSubscriptionId);
|
||||
const azureResourceGroup = useAppSelector(selectAzureResourceGroup);
|
||||
const azureSource = useAppSelector(selectAzureSource);
|
||||
// Registration
|
||||
const registrationValidation = useRegistrationValidation();
|
||||
// Snapshots
|
||||
|
|
@ -286,8 +288,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
const firewallValidation = useFirewallValidation();
|
||||
// Services
|
||||
const servicesValidation = useServicesValidation();
|
||||
// AAP
|
||||
const aapValidation = useAAPValidation();
|
||||
// Firstboot
|
||||
const firstBootValidation = useFirstBootValidation();
|
||||
// Details
|
||||
|
|
@ -295,51 +295,45 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
// Users
|
||||
const usersValidation = useUsersValidation();
|
||||
|
||||
const hasWslTargetOnly = useHasSpecificTargetOnly('wsl');
|
||||
|
||||
let startIndex = 1; // default index
|
||||
const JUMP_TO_REVIEW_STEP = 23;
|
||||
|
||||
if (isEdit) {
|
||||
startIndex = JUMP_TO_REVIEW_STEP;
|
||||
startIndex = 22;
|
||||
}
|
||||
|
||||
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
|
||||
|
||||
// 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
|
||||
const CustomStatusNavItem = (
|
||||
const customStatusNavItem = (
|
||||
step: WizardStepType,
|
||||
activeStep: WizardStepType,
|
||||
steps: WizardStepType[],
|
||||
goToStepByIndex: (index: number) => void,
|
||||
goToStepByIndex: (index: number) => void
|
||||
) => {
|
||||
const isVisitOptional =
|
||||
'parentId' in step && step.parentId === 'step-optional-steps';
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.IS_ON_PREMISE) {
|
||||
if (step.id === 'step-oscap' && step.isVisited) {
|
||||
setWasRegisterVisited(true);
|
||||
}
|
||||
} else if (step.id === 'step-register' && step.isVisited) {
|
||||
if (process.env.IS_ON_PREMISE) {
|
||||
if (step.id === 'step-oscap' && step.isVisited) {
|
||||
setWasRegisterVisited(true);
|
||||
}
|
||||
}, [step.id, step.isVisited]);
|
||||
} else if (step.id === 'step-register' && step.isVisited) {
|
||||
setWasRegisterVisited(true);
|
||||
}
|
||||
|
||||
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
|
||||
const status = (step.id !== activeStep.id && step.status) || 'default';
|
||||
const status = (step?.id !== activeStep?.id && step?.status) || 'default';
|
||||
|
||||
return (
|
||||
<WizardNavItem
|
||||
key={step.id}
|
||||
id={step.id}
|
||||
key={step?.id}
|
||||
id={step?.id}
|
||||
content={step.name}
|
||||
isCurrent={activeStep.id === step.id}
|
||||
isCurrent={activeStep?.id === step?.id}
|
||||
isDisabled={
|
||||
step.isDisabled ||
|
||||
(!step.isVisited &&
|
||||
|
|
@ -356,7 +350,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
{
|
||||
module: AMPLITUDE_MODULE_NAME,
|
||||
isPreview: isBeta(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
|
@ -375,8 +369,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
isVisitRequired
|
||||
>
|
||||
<WizardStep
|
||||
name='Image output'
|
||||
id='step-image-output'
|
||||
name="Image output"
|
||||
id="step-image-output"
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={targetEnvironments.length === 0}
|
||||
|
|
@ -387,29 +381,25 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<ImageOutputStep />
|
||||
</WizardStep>
|
||||
<WizardStep
|
||||
name='Target Environment'
|
||||
id='step-target-environment'
|
||||
name="Target Environment"
|
||||
id="step-target-environment"
|
||||
isHidden={
|
||||
!targetEnvironments.find(
|
||||
(target: string) =>
|
||||
target === 'aws' || target === 'gcp' || target === 'azure',
|
||||
(target) =>
|
||||
target === 'aws' || target === 'gcp' || target === 'azure'
|
||||
)
|
||||
}
|
||||
steps={[
|
||||
<WizardStep
|
||||
name='Amazon Web Services'
|
||||
id='wizard-target-aws'
|
||||
key='wizard-target-aws'
|
||||
name="Amazon Web Services"
|
||||
id="wizard-target-aws"
|
||||
key="wizard-target-aws"
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={
|
||||
// we don't need the account id for
|
||||
// on-prem aws.
|
||||
process.env.IS_ON_PREMISE
|
||||
? false
|
||||
: awsShareMethod === 'manual'
|
||||
? !isAwsAccountIdValid(awsAccountId)
|
||||
: awsSourceId === undefined
|
||||
awsShareMethod === 'manual'
|
||||
? !isAwsAccountIdValid(awsAccountId)
|
||||
: awsSourceId === undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
|
@ -418,9 +408,9 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<Aws />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Google Cloud Platform'
|
||||
id='wizard-target-gcp'
|
||||
key='wizard-target-gcp'
|
||||
name="Google Cloud Platform"
|
||||
id="wizard-target-gcp"
|
||||
key="wizard-target-gcp"
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={
|
||||
|
|
@ -434,15 +424,21 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<Gcp />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Azure'
|
||||
id='wizard-target-azure'
|
||||
key='wizard-target-azure'
|
||||
name="Azure"
|
||||
id="wizard-target-azure"
|
||||
key="wizard-target-azure"
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={
|
||||
!isAzureTenantGUIDValid(azureTenantId) ||
|
||||
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
|
||||
!isAzureResourceGroupValid(azureResourceGroup)
|
||||
azureShareMethod === 'manual'
|
||||
? !isAzureTenantGUIDValid(azureTenantId) ||
|
||||
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
|
||||
!isAzureResourceGroupValid(azureResourceGroup)
|
||||
: azureShareMethod === 'sources'
|
||||
? !isAzureTenantGUIDValid(azureTenantId) ||
|
||||
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
|
||||
!isAzureResourceGroupValid(azureResourceGroup)
|
||||
: azureSource === undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
|
@ -453,15 +449,15 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
]}
|
||||
/>
|
||||
<WizardStep
|
||||
name='Optional steps'
|
||||
id='step-optional-steps'
|
||||
name="Optional steps"
|
||||
id="step-optional-steps"
|
||||
steps={[
|
||||
<WizardStep
|
||||
name='Register'
|
||||
id='step-register'
|
||||
key='step-register'
|
||||
name="Register"
|
||||
id="step-register"
|
||||
key="step-register"
|
||||
isHidden={!!process.env.IS_ON_PREMISE || !isRhel(distribution)}
|
||||
navItem={CustomStatusNavItem}
|
||||
navItem={customStatusNavItem}
|
||||
status={
|
||||
wasRegisterVisited
|
||||
? registrationValidation.disabledNext
|
||||
|
|
@ -480,9 +476,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
</WizardStep>,
|
||||
<WizardStep
|
||||
name={complianceEnabled ? 'Compliance' : 'OpenSCAP'}
|
||||
id='step-oscap'
|
||||
key='step-oscap'
|
||||
navItem={CustomStatusNavItem}
|
||||
id="step-oscap"
|
||||
key="step-oscap"
|
||||
isHidden={distribution === RHEL_10_BETA}
|
||||
navItem={customStatusNavItem}
|
||||
footer={
|
||||
<CustomWizardFooter disableNext={false} optional={true} />
|
||||
}
|
||||
|
|
@ -490,11 +487,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<OscapStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='File system configuration'
|
||||
id='step-file-system'
|
||||
key='step-file-system'
|
||||
navItem={CustomStatusNavItem}
|
||||
isHidden={hasWslTargetOnly}
|
||||
name="File system configuration"
|
||||
id="step-file-system"
|
||||
key="step-file-system"
|
||||
navItem={customStatusNavItem}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
beforeNext={() => {
|
||||
|
|
@ -516,12 +512,16 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
</FileSystemContext.Provider>
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Repeatable build'
|
||||
id='wizard-repository-snapshot'
|
||||
key='wizard-repository-snapshot'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="Repeatable build"
|
||||
id="wizard-repository-snapshot"
|
||||
key="wizard-repository-snapshot"
|
||||
navItem={customStatusNavItem}
|
||||
status={snapshotValidation.disabledNext ? 'error' : 'default'}
|
||||
isHidden={!!process.env.IS_ON_PREMISE}
|
||||
isHidden={
|
||||
distribution === RHEL_10_BETA ||
|
||||
!!process.env.IS_ON_PREMISE ||
|
||||
isFedoraEnv
|
||||
}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={snapshotValidation.disabledNext}
|
||||
|
|
@ -532,11 +532,15 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<SnapshotStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Custom repositories'
|
||||
id='wizard-custom-repositories'
|
||||
key='wizard-custom-repositories'
|
||||
navItem={CustomStatusNavItem}
|
||||
isHidden={!!process.env.IS_ON_PREMISE}
|
||||
name="Custom repositories"
|
||||
id="wizard-custom-repositories"
|
||||
key="wizard-custom-repositories"
|
||||
navItem={customStatusNavItem}
|
||||
isHidden={
|
||||
distribution === RHEL_10_BETA ||
|
||||
!!process.env.IS_ON_PREMISE ||
|
||||
isFedoraEnv
|
||||
}
|
||||
isDisabled={snapshotValidation.disabledNext}
|
||||
footer={
|
||||
<CustomWizardFooter disableNext={false} optional={true} />
|
||||
|
|
@ -545,10 +549,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<RepositoriesStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Additional packages'
|
||||
id='wizard-additional-packages'
|
||||
key='wizard-additional-packages'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="Additional packages"
|
||||
id="wizard-additional-packages"
|
||||
key="wizard-additional-packages"
|
||||
navItem={customStatusNavItem}
|
||||
isHidden={isFedoraEnv}
|
||||
isDisabled={snapshotValidation.disabledNext}
|
||||
footer={
|
||||
<CustomWizardFooter disableNext={false} optional={true} />
|
||||
|
|
@ -557,10 +562,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<PackagesStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Users'
|
||||
id='wizard-users'
|
||||
key='wizard-users'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="Users"
|
||||
id="wizard-users"
|
||||
key="wizard-users"
|
||||
navItem={customStatusNavItem}
|
||||
isHidden={!isUsersEnabled}
|
||||
status={usersValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -572,10 +578,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<UsersStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Timezone'
|
||||
id='wizard-timezone'
|
||||
key='wizard-timezone'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="Timezone"
|
||||
id="wizard-timezone"
|
||||
key="wizard-timezone"
|
||||
navItem={customStatusNavItem}
|
||||
status={timezoneValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -587,10 +593,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<TimezoneStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Locale'
|
||||
id='wizard-locale'
|
||||
key='wizard-locale'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="Locale"
|
||||
id="wizard-locale"
|
||||
key="wizard-locale"
|
||||
navItem={customStatusNavItem}
|
||||
status={localeValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -602,10 +608,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<LocaleStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Hostname'
|
||||
id='wizard-hostname'
|
||||
key='wizard-hostname'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="Hostname"
|
||||
id="wizard-hostname"
|
||||
key="wizard-hostname"
|
||||
navItem={customStatusNavItem}
|
||||
status={hostnameValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -617,11 +623,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<HostnameStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Kernel'
|
||||
id='wizard-kernel'
|
||||
key='wizard-kernel'
|
||||
navItem={CustomStatusNavItem}
|
||||
isHidden={hasWslTargetOnly}
|
||||
name="Kernel"
|
||||
id="wizard-kernel"
|
||||
key="wizard-kernel"
|
||||
navItem={customStatusNavItem}
|
||||
status={kernelValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -633,10 +638,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<KernelStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Firewall'
|
||||
id='wizard-firewall'
|
||||
key='wizard-firewall'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="Firewall"
|
||||
id="wizard-firewall"
|
||||
key="wizard-firewall"
|
||||
navItem={customStatusNavItem}
|
||||
status={firewallValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -648,10 +653,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<FirewallStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Systemd services'
|
||||
id='wizard-services'
|
||||
key='wizard-services'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="Systemd services"
|
||||
id="wizard-services"
|
||||
key="wizard-services"
|
||||
navItem={customStatusNavItem}
|
||||
status={servicesValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -663,28 +668,12 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<ServicesStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Ansible Automation Platform'
|
||||
id='wizard-aap'
|
||||
isHidden={!isAAPRegistrationEnabled}
|
||||
key='wizard-aap'
|
||||
navItem={CustomStatusNavItem}
|
||||
status={aapValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={aapValidation.disabledNext}
|
||||
optional={true}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AAPStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='First boot script configuration'
|
||||
id='wizard-first-boot'
|
||||
key='wizard-first-boot'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="First boot script configuration"
|
||||
id="wizard-first-boot"
|
||||
key="wizard-first-boot"
|
||||
navItem={customStatusNavItem}
|
||||
status={firstBootValidation.disabledNext ? 'error' : 'default'}
|
||||
isHidden={!!process.env.IS_ON_PREMISE}
|
||||
isHidden={!!process.env.IS_ON_PREMISE || isFedoraEnv}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={firstBootValidation.disabledNext}
|
||||
|
|
@ -697,9 +686,9 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
]}
|
||||
/>
|
||||
<WizardStep
|
||||
name='Details'
|
||||
id='step-details'
|
||||
navItem={CustomStatusNavItem}
|
||||
name="Details"
|
||||
id="step-details"
|
||||
navItem={customStatusNavItem}
|
||||
status={detailsValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -710,8 +699,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<DetailsStep />
|
||||
</WizardStep>
|
||||
<WizardStep
|
||||
name='Review'
|
||||
id='step-review'
|
||||
name="Review"
|
||||
id="step-review"
|
||||
footer={<ReviewWizardFooter />}
|
||||
>
|
||||
<ReviewStep />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 CreateImageWizard from './CreateImageWizard';
|
||||
|
|
@ -13,18 +13,19 @@ const ImportImageWizard = () => {
|
|||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const addNotification = useAddNotification();
|
||||
const locationState = location.state as { blueprint?: wizardState };
|
||||
const blueprint = locationState.blueprint;
|
||||
const blueprint = locationState?.blueprint;
|
||||
useEffect(() => {
|
||||
if (blueprint) {
|
||||
dispatch(loadWizardState(blueprint));
|
||||
} else {
|
||||
navigate(resolveRelPath(''));
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'No blueprint was imported',
|
||||
});
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'No blueprint was imported',
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [blueprint, dispatch]);
|
||||
return <CreateImageWizard />;
|
||||
|
|
|
|||
|
|
@ -48,73 +48,30 @@ const LabelInput = ({
|
|||
const dispatch = useAppDispatch();
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [onStepInputErrorText, setOnStepInputErrorText] = useState('');
|
||||
let [invalidImports, duplicateImports] = ['', ''];
|
||||
|
||||
if (stepValidation.errors[fieldName]) {
|
||||
[invalidImports, duplicateImports] =
|
||||
stepValidation.errors[fieldName].split('|');
|
||||
}
|
||||
const [errorText, setErrorText] = useState(stepValidation.errors[fieldName]);
|
||||
|
||||
const onTextInputChange = (
|
||||
_event: React.FormEvent<HTMLInputElement>,
|
||||
value: string,
|
||||
value: string
|
||||
) => {
|
||||
setInputValue(value);
|
||||
setOnStepInputErrorText('');
|
||||
setErrorText('');
|
||||
};
|
||||
|
||||
const addItem = (value: string) => {
|
||||
if (list?.includes(value) || requiredList?.includes(value)) {
|
||||
setOnStepInputErrorText(`${item} already exists.`);
|
||||
setErrorText(`${item} already exists.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator(value)) {
|
||||
switch (fieldName) {
|
||||
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.');
|
||||
}
|
||||
setErrorText('Invalid format.');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(addAction(value));
|
||||
setInputValue('');
|
||||
setOnStepInputErrorText('');
|
||||
setErrorText('');
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent, value: string) => {
|
||||
|
|
@ -130,18 +87,14 @@ const LabelInput = ({
|
|||
|
||||
const handleRemoveItem = (e: React.MouseEvent, value: string) => {
|
||||
dispatch(removeAction(value));
|
||||
setErrorText('');
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setInputValue('');
|
||||
setOnStepInputErrorText('');
|
||||
setErrorText('');
|
||||
};
|
||||
|
||||
const errors = [];
|
||||
if (onStepInputErrorText) errors.push(onStepInputErrorText);
|
||||
if (invalidImports) errors.push(invalidImports);
|
||||
if (duplicateImports) errors.push(duplicateImports);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInputGroup>
|
||||
|
|
@ -154,38 +107,34 @@ const LabelInput = ({
|
|||
<TextInputGroupUtilities>
|
||||
<Button
|
||||
icon={
|
||||
<Icon status='info'>
|
||||
<Icon status="info">
|
||||
<PlusCircleIcon />
|
||||
</Icon>
|
||||
}
|
||||
variant='plain'
|
||||
variant="plain"
|
||||
onClick={(e) => handleAddItem(e, inputValue)}
|
||||
isDisabled={!inputValue}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
<Button
|
||||
icon={<TimesIcon />}
|
||||
variant='plain'
|
||||
variant="plain"
|
||||
onClick={handleClear}
|
||||
isDisabled={!inputValue}
|
||||
aria-label='Clear input'
|
||||
aria-label="Clear input"
|
||||
/>
|
||||
</TextInputGroupUtilities>
|
||||
</TextInputGroup>
|
||||
{errors.length > 0 && (
|
||||
{errorText && (
|
||||
<HelperText>
|
||||
{errors.map((error, index) => (
|
||||
<HelperTextItem key={index} variant={'error'}>
|
||||
{error}
|
||||
</HelperTextItem>
|
||||
))}
|
||||
<HelperTextItem variant={'error'}>{errorText}</HelperTextItem>
|
||||
</HelperText>
|
||||
)}
|
||||
{requiredList && requiredList.length > 0 && (
|
||||
<LabelGroup
|
||||
categoryName={requiredCategoryName}
|
||||
numLabels={20}
|
||||
className='pf-v6-u-mt-sm pf-v6-u-w-100'
|
||||
className="pf-v6-u-mt-sm pf-v6-u-w-100"
|
||||
>
|
||||
{requiredList.map((item) => (
|
||||
<Label key={item} isCompact>
|
||||
|
|
@ -194,7 +143,7 @@ const LabelInput = ({
|
|||
))}
|
||||
</LabelGroup>
|
||||
)}
|
||||
<LabelGroup numLabels={20} className='pf-v6-u-mt-sm pf-v6-u-w-100'>
|
||||
<LabelGroup numLabels={20} className="pf-v6-u-mt-sm pf-v6-u-w-100">
|
||||
{list?.map((item) => (
|
||||
<Label
|
||||
key={item}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { Alert } from '@patternfly/react-core';
|
|||
const UsrSubDirectoriesDisabled = () => {
|
||||
return (
|
||||
<Alert
|
||||
variant='warning'
|
||||
title='Sub-directories for the /usr mount point are no longer supported'
|
||||
variant="warning"
|
||||
title="Sub-directories for the /usr mount point are no longer supported"
|
||||
isInline
|
||||
>
|
||||
Please note that including sub-directories in the /usr path is no longer
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ type ValidatedTextInputPropTypes = TextInputProps & {
|
|||
type ValidationInputProp = TextInputProps &
|
||||
TextAreaProps & {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
placeholder: string;
|
||||
stepValidation: StepValidation;
|
||||
dataTestId?: string;
|
||||
fieldName: string;
|
||||
|
|
@ -31,7 +31,7 @@ type ValidationInputProp = TextInputProps &
|
|||
ariaLabel: string;
|
||||
onChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
value: string,
|
||||
value: string
|
||||
) => void;
|
||||
isRequired?: boolean;
|
||||
warning?: string;
|
||||
|
|
@ -91,14 +91,14 @@ export const ValidatedInputAndTextArea = ({
|
|||
onChange={onChange}
|
||||
validated={validated}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder || ''}
|
||||
placeholder={placeholder}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
)}
|
||||
{warning !== undefined && warning !== '' && (
|
||||
<HelperText>
|
||||
<HelperTextItem variant='warning'>{warning}</HelperTextItem>
|
||||
<HelperTextItem variant="warning">{warning}</HelperTextItem>
|
||||
</HelperText>
|
||||
)}
|
||||
{validated === 'error' && hasError && (
|
||||
|
|
@ -111,13 +111,13 @@ export const ValidatedInputAndTextArea = ({
|
|||
const getValidationState = (
|
||||
isPristine: boolean,
|
||||
errorMessage: string,
|
||||
isRequired: boolean | undefined,
|
||||
isRequired: boolean | undefined
|
||||
): ValidationResult => {
|
||||
const validated = isPristine
|
||||
? 'default'
|
||||
: (isRequired && errorMessage) || errorMessage
|
||||
? 'error'
|
||||
: 'success';
|
||||
? 'error'
|
||||
: 'success';
|
||||
|
||||
return validated;
|
||||
};
|
||||
|
|
@ -125,7 +125,7 @@ const getValidationState = (
|
|||
export const ErrorMessage = ({ errorMessage }: ErrorMessageProps) => {
|
||||
return (
|
||||
<HelperText>
|
||||
<HelperTextItem variant='error'>{errorMessage}</HelperTextItem>
|
||||
<HelperTextItem variant="error">{errorMessage}</HelperTextItem>
|
||||
</HelperText>
|
||||
);
|
||||
};
|
||||
|
|
@ -138,7 +138,6 @@ export const ValidatedInput = ({
|
|||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
...props
|
||||
}: ValidatedTextInputPropTypes) => {
|
||||
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
||||
|
||||
|
|
@ -159,17 +158,16 @@ export const ValidatedInput = ({
|
|||
<TextInput
|
||||
value={value}
|
||||
data-testid={dataTestId}
|
||||
type='text'
|
||||
type="text"
|
||||
onChange={onChange!}
|
||||
validated={handleValidation()}
|
||||
aria-label={ariaLabel || ''}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder || ''}
|
||||
{...props}
|
||||
/>
|
||||
{!isPristine && !validator(value) && (
|
||||
<HelperText>
|
||||
<HelperTextItem variant='error'>{helperText}</HelperTextItem>
|
||||
<HelperTextItem variant="error">{helperText}</HelperTextItem>
|
||||
</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 {
|
||||
Content,
|
||||
Form,
|
||||
FormGroup,
|
||||
FormHelperText,
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
Content,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ const DetailsStep = () => {
|
|||
|
||||
const handleNameChange = (
|
||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
name: string,
|
||||
name: string
|
||||
) => {
|
||||
dispatch(changeBlueprintName(name));
|
||||
dispatch(setIsCustomName());
|
||||
|
|
@ -36,7 +36,7 @@ const DetailsStep = () => {
|
|||
|
||||
const handleDescriptionChange = (
|
||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
description: string,
|
||||
description: string
|
||||
) => {
|
||||
dispatch(changeBlueprintDescription(description));
|
||||
};
|
||||
|
|
@ -45,7 +45,7 @@ const DetailsStep = () => {
|
|||
|
||||
return (
|
||||
<Form>
|
||||
<Title headingLevel='h1' size='xl'>
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Details
|
||||
</Title>
|
||||
<Content>
|
||||
|
|
@ -53,15 +53,15 @@ const DetailsStep = () => {
|
|||
images created from this blueprint will use the name of the parent
|
||||
blueprint.
|
||||
</Content>
|
||||
<FormGroup isRequired label='Blueprint name' fieldId='blueprint-name'>
|
||||
<FormGroup isRequired label="Blueprint name" fieldId="blueprint-name">
|
||||
<ValidatedInputAndTextArea
|
||||
ariaLabel='blueprint name'
|
||||
dataTestId='blueprint'
|
||||
ariaLabel="blueprint name"
|
||||
dataTestId="blueprint"
|
||||
value={blueprintName}
|
||||
onChange={handleNameChange}
|
||||
placeholder='Add blueprint name'
|
||||
placeholder="Add blueprint name"
|
||||
stepValidation={stepValidation}
|
||||
fieldName='name'
|
||||
fieldName="name"
|
||||
isRequired={true}
|
||||
/>
|
||||
<FormHelperText>
|
||||
|
|
@ -75,17 +75,17 @@ const DetailsStep = () => {
|
|||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label='Blueprint description'
|
||||
fieldId='blueprint-description-name'
|
||||
label="Blueprint description"
|
||||
fieldId="blueprint-description-name"
|
||||
>
|
||||
<ValidatedInputAndTextArea
|
||||
ariaLabel='blueprint description'
|
||||
dataTestId='blueprint description'
|
||||
ariaLabel="blueprint description"
|
||||
dataTestId="blueprint description"
|
||||
value={blueprintDescription}
|
||||
onChange={handleDescriptionChange}
|
||||
placeholder='Add description'
|
||||
placeholder="Add description"
|
||||
stepValidation={stepValidation}
|
||||
fieldName='description'
|
||||
fieldName="description"
|
||||
/>
|
||||
</FormGroup>
|
||||
</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