Compare commits
297 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
080513ad6d | ||
|
|
7391652e17 | ||
|
|
9a17373234 | ||
|
|
d7f844b8b6 | ||
|
|
859b7cace8 | ||
|
|
3a83a14720 | ||
|
|
e61cb99f1b | ||
|
|
a5aa15cbcb | ||
|
|
44c3674072 | ||
|
|
4d783537fb | ||
|
|
0b96c64c93 | ||
|
|
0d917c3cd8 | ||
|
|
957700adcc | ||
|
|
fa0560ac4d | ||
|
|
0e7f5d9e7b | ||
|
|
e0dd33fdc9 | ||
|
|
b0393a5f4f | ||
|
|
a9d2ba59a8 | ||
|
|
af19251f17 | ||
|
|
090544c333 | ||
|
|
4b188a0393 | ||
|
|
63f55c7408 | ||
|
|
bc3288a83e | ||
|
|
3e5c5dca76 | ||
|
|
04f0528701 | ||
|
|
3e2e9dcaa6 | ||
|
|
54e413f459 | ||
|
|
f6f6e58449 | ||
|
|
bf77501eea | ||
|
|
42b16bafd8 | ||
|
|
04adcc133c | ||
|
|
11e352440f | ||
|
|
122c481c09 | ||
|
|
c930621316 | ||
|
|
cedb4f07bd | ||
|
|
0c47c4b165 | ||
|
|
223d11b691 | ||
|
|
62d18b2a38 | ||
|
|
df405033d5 | ||
|
|
771abb8bc9 | ||
|
|
dea68f8b5c | ||
|
|
f668d295a6 | ||
|
|
0af6a0324f | ||
|
|
eee1f78d27 | ||
|
|
d66f54a847 | ||
|
|
3461c908fb | ||
|
|
b08fee11bc | ||
|
|
e5de087810 | ||
|
|
0c46f052a8 | ||
|
|
904e4cccea | ||
|
|
c868fe5d41 | ||
|
|
676ffc9b3a | ||
|
|
30f4cdd9c3 | ||
|
|
8209bfe62c | ||
|
|
2098ede032 | ||
|
|
bb345c0e4f | ||
|
|
eafcd200ae | ||
|
|
e9025e460c | ||
|
|
4c098db796 | ||
|
|
2bea0bd50b | ||
|
|
894d2a4d76 | ||
|
|
68b2f74a97 | ||
|
|
35c9f32cf8 | ||
|
|
7269b0c7db | ||
|
|
88dd0880c8 | ||
|
|
4f250ee637 | ||
|
|
acc79e149c | ||
|
|
3b8b2ad240 | ||
|
|
9f526faa65 | ||
|
|
0acedb913c | ||
|
|
327e1cd48f | ||
|
|
1bfc830147 | ||
|
|
fdaf5129c8 | ||
|
|
64e5744d8c | ||
|
|
90c2c65ebe | ||
|
|
9943f54cd9 | ||
|
|
d94834e25f | ||
|
|
9efdd82771 | ||
|
|
1096c6d4fb | ||
|
|
d5321bb078 | ||
|
|
8cf161d4e5 | ||
|
|
98b890206b | ||
|
|
8d656b766c | ||
|
|
ceec85209c | ||
|
|
cfa8cbcb28 | ||
|
|
96da1817df | ||
|
|
9da490ad52 | ||
|
|
128abcb98f | ||
|
|
c026102dd3 | ||
|
|
d5877b256c | ||
|
|
730554dc84 | ||
|
|
09febf8061 | ||
|
|
8524e8e374 | ||
|
|
a8f21a7a90 | ||
|
|
e52b43bdb7 | ||
|
|
2c3efe4c04 | ||
|
|
ad5ea22da8 | ||
|
|
9f3ad99037 | ||
|
|
f4e872548c | ||
|
|
5b7b8daa4d | ||
|
|
67a0f86dde | ||
|
|
4d051eecde | ||
|
|
661fd29a5e | ||
|
|
6bf800d4d9 | ||
|
|
825e3beac1 | ||
|
|
6f9a34c972 | ||
|
|
42d96edd00 | ||
|
|
b8dc0e60c9 | ||
|
|
5c1f9dbbdd | ||
|
|
3d39065ad0 | ||
|
|
f86f81d6d5 | ||
|
|
690b71636a | ||
|
|
a6eadbffac | ||
|
|
8af4181ae9 | ||
|
|
3a9e3aa200 | ||
|
|
4339420cb8 | ||
|
|
fe5abaeb45 | ||
|
|
c88171da19 | ||
|
|
2f765a1d4b | ||
|
|
2ce62d4ef0 | ||
|
|
253317497e | ||
|
|
8dd82d5801 | ||
|
|
33d3a02ee5 | ||
|
|
1621042a08 | ||
|
|
786b334573 | ||
|
|
c67eefbe25 | ||
|
|
520b94099e | ||
|
|
4fe4872be4 | ||
|
|
f40e67a98a | ||
|
|
69751cba1a | ||
|
|
06a5db21e0 | ||
|
|
016cf0b4f3 | ||
|
|
654458c12d | ||
|
|
49fef039c0 | ||
|
|
e657b88bfc | ||
|
|
cdca105c97 | ||
|
|
78ea07d777 | ||
|
|
c550ba1ae8 | ||
|
|
d03f41f160 | ||
|
|
0373a55f8c | ||
|
|
eeae4f9467 | ||
|
|
4c04f2dc54 | ||
|
|
10ff517ab0 | ||
|
|
25a5f140d8 | ||
|
|
91b2cc2d10 | ||
|
|
a0fe3644c3 | ||
|
|
0ea874abc6 | ||
|
|
2406d14304 | ||
|
|
c2e94100db | ||
|
|
ccfdb49db2 | ||
|
|
3f35101f68 | ||
|
|
10a40aaec4 | ||
|
|
c9d721ea52 | ||
|
|
0b0171bb87 | ||
|
|
1ed4380bfc | ||
|
|
5afe1c1fc1 | ||
|
|
ca6c59bfb8 | ||
|
|
09df007eb9 | ||
|
|
c55706b931 | ||
|
|
afcc0126e4 | ||
|
|
73ffb97414 | ||
|
|
ecc1c2c8cd | ||
|
|
d7945a458a | ||
|
|
87647f8854 | ||
|
|
9d2c798376 | ||
|
|
1e545af0c7 | ||
|
|
719ee1a024 | ||
|
|
146a9131b4 | ||
|
|
fd474dace0 | ||
|
|
b6e9fef70b | ||
|
|
10a67ca6e3 | ||
|
|
d0aa6d733e | ||
|
|
a2da15b9d2 | ||
|
|
6beae64f60 | ||
|
|
3e115228e4 | ||
|
|
c17b54c68c | ||
|
|
74817d87de | ||
|
|
0bf939f2d0 | ||
|
|
9ffbf67c42 | ||
|
|
8c4e8d4fab | ||
|
|
bb9c5620ee | ||
|
|
a92d087014 | ||
|
|
33b374b3a0 | ||
|
|
94bcb9672f | ||
|
|
875b8a150d | ||
|
|
8abeb4f53b | ||
|
|
5c93300927 | ||
|
|
9f8b271b7d | ||
|
|
9a29eeb28d | ||
|
|
3f1a80fbe2 | ||
|
|
855f1430ad | ||
|
|
0b1abb57b9 | ||
|
|
6521a46bb1 | ||
|
|
caa678ebeb | ||
|
|
ea93498ef2 | ||
|
|
5d6c6dc58b | ||
|
|
bf952c5c7a | ||
|
|
6ae8b3e740 | ||
|
|
eaead88a78 | ||
|
|
8d106499fd | ||
|
|
2c29290212 | ||
|
|
9367bb4b28 | ||
|
|
6a62e71ec8 | ||
|
|
779c50762f | ||
|
|
02cafcc29a | ||
|
|
9350d4ee6b | ||
|
|
1f34e95469 | ||
|
|
e8d46dd716 | ||
|
|
77e0f5d6bf | ||
|
|
eafb9ea0f3 | ||
|
|
852d24e568 | ||
|
|
6cefc6c199 | ||
|
|
eca4e55c67 | ||
|
|
9247ea6196 | ||
|
|
e05079330b | ||
|
|
12024b08c6 | ||
|
|
d7c2202b11 | ||
|
|
fc874422de | ||
|
|
2894858838 | ||
|
|
0319c81b41 | ||
|
|
0597541af2 | ||
|
|
969497e722 | ||
|
|
839559d42c | ||
|
|
4ead145e38 | ||
|
|
35485fc163 | ||
|
|
2d8da339e7 | ||
|
|
f6b1971760 | ||
|
|
2f2f40c4b7 | ||
|
|
2f034dffd8 | ||
|
|
1ea1c2de8c | ||
|
|
47aace0c5f | ||
|
|
c98659fbd7 | ||
|
|
b9fdb9946a | ||
|
|
981b62d7b0 | ||
|
|
3e4ee6891d | ||
|
|
064aa172a0 | ||
|
|
7f06002b26 | ||
|
|
1a65c0c3d4 | ||
|
|
434960e0fc | ||
|
|
0d65220826 | ||
|
|
626ebaa3c7 | ||
|
|
874f5dd040 | ||
|
|
6eaf2f9862 | ||
|
|
4b411c9a27 | ||
|
|
f44afe2723 | ||
|
|
2a611e9704 | ||
|
|
b499dfcf93 | ||
|
|
78bb1e118b | ||
|
|
c2998306cf | ||
|
|
bac647ded6 | ||
|
|
16e5bdbe3a | ||
|
|
66ed82a531 | ||
|
|
1d3967a585 | ||
|
|
cdd10a01ff | ||
|
|
5545ce4027 | ||
|
|
362bfd393b | ||
|
|
df5388dae8 | ||
|
|
b59a729656 | ||
|
|
0ce28044b8 | ||
|
|
b7860f33fc | ||
|
|
07f500b94a | ||
|
|
76320925a0 | ||
|
|
235d853f42 | ||
|
|
a4ac280350 | ||
|
|
47d526cf5c | ||
|
|
9189a20e57 | ||
|
|
4667f6b0ac | ||
|
|
cd137fb055 | ||
|
|
9478958085 | ||
|
|
564c5461d4 | ||
|
|
0cfe3dde30 | ||
|
|
50d88e5949 | ||
|
|
cb08466734 | ||
|
|
139dd367fe | ||
|
|
2f8b550408 | ||
|
|
31d259e988 | ||
|
|
798d994ad0 | ||
|
|
8e504a527b | ||
|
|
d6acce47a2 | ||
|
|
cb8c8a3d5c | ||
|
|
c59cde1ab9 | ||
|
|
b465920b18 | ||
|
|
3312beb6e7 | ||
|
|
8f0c53138e | ||
|
|
4e19ccc5e9 | ||
|
|
54b6877f95 | ||
|
|
7f5013ef07 | ||
|
|
1ba0f33240 | ||
|
|
4932ba6909 | ||
|
|
84bc0f92a0 | ||
|
|
e7bf1d3540 | ||
|
|
40fe892dbf | ||
|
|
3dd67c8f39 | ||
|
|
c29cee781f | ||
|
|
2b37ee998e | ||
|
|
34c3dc614d | ||
|
|
56b22eee53 |
318 changed files with 13969 additions and 46611 deletions
|
|
@ -1,8 +0,0 @@
|
|||
# Ignore programatically generated API slices
|
||||
imageBuilderApi.ts
|
||||
contentSourcesApi.ts
|
||||
rhsmApi.ts
|
||||
provisioningApi.ts
|
||||
edgeApi.ts
|
||||
complianceApi.ts
|
||||
composerCloudApi.ts
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
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
.fmf/version
Normal file
1
.fmf/version
Normal file
|
|
@ -0,0 +1 @@
|
|||
1
|
||||
257
.forgejo/workflows/ci.yml
Normal file
257
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
---
|
||||
name: Debian Image Builder Frontend CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18"
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build and Test Frontend
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:18-bullseye
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
git \
|
||||
ca-certificates \
|
||||
python3
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: |
|
||||
npm ci
|
||||
npm run build || echo "Build script not found"
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
if [ -f package.json ] && npm run test; then
|
||||
npm test
|
||||
else
|
||||
echo "No test script found, skipping tests"
|
||||
fi
|
||||
|
||||
- name: Run linting
|
||||
run: |
|
||||
if [ -f package.json ] && npm run lint; then
|
||||
npm run lint
|
||||
else
|
||||
echo "No lint script found, skipping linting"
|
||||
fi
|
||||
|
||||
- name: Build production bundle
|
||||
run: |
|
||||
if [ -f package.json ] && npm run build; then
|
||||
npm run build
|
||||
else
|
||||
echo "No build script found"
|
||||
fi
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-build
|
||||
path: |
|
||||
dist/
|
||||
build/
|
||||
retention-days: 30
|
||||
|
||||
package:
|
||||
name: Package Frontend
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:18-bullseye
|
||||
needs: build-and-test
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
devscripts \
|
||||
debhelper \
|
||||
git \
|
||||
ca-certificates \
|
||||
python3
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build production bundle
|
||||
run: |
|
||||
if [ -f package.json ] && npm run build; then
|
||||
npm run build
|
||||
else
|
||||
echo "No build script found"
|
||||
fi
|
||||
|
||||
- name: Create debian directory
|
||||
run: |
|
||||
mkdir -p debian
|
||||
cat > debian/control << EOF
|
||||
Source: debian-image-builder-frontend
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Debian Forge Team <team@debian-forge.org>
|
||||
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
||||
Standards-Version: 4.6.2
|
||||
|
||||
Package: debian-image-builder-frontend
|
||||
Architecture: all
|
||||
Depends: \${misc:Depends}, nodejs, nginx
|
||||
Description: Debian Image Builder Frontend
|
||||
Web-based frontend for Debian Image Builder with Cockpit integration.
|
||||
Provides a user interface for managing image builds, blueprints,
|
||||
and system configurations through a modern React application.
|
||||
EOF
|
||||
|
||||
cat > debian/rules << EOF
|
||||
#!/usr/bin/make -f
|
||||
%:
|
||||
dh \$@
|
||||
|
||||
override_dh_auto_install:
|
||||
dh_auto_install
|
||||
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
||||
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
||||
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
||||
|
||||
# Copy built frontend files
|
||||
if [ -d dist ]; then
|
||||
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||
elif [ -d build ]; then
|
||||
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||
fi
|
||||
|
||||
# Copy source files for development
|
||||
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||
|
||||
# Create nginx configuration
|
||||
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/debian-image-builder-frontend;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8080/;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
}
|
||||
}
|
||||
NGINX_EOF
|
||||
|
||||
# Create cockpit manifest
|
||||
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
||||
{
|
||||
"version": 1,
|
||||
"manifest": {
|
||||
"name": "debian-image-builder",
|
||||
"version": "1.0.0",
|
||||
"title": "Debian Image Builder",
|
||||
"description": "Build and manage Debian atomic images",
|
||||
"url": "/usr/share/debian-image-builder-frontend",
|
||||
"icon": "debian-logo",
|
||||
"requires": {
|
||||
"cockpit": ">= 200"
|
||||
}
|
||||
}
|
||||
}
|
||||
COCKPIT_EOF
|
||||
EOF
|
||||
|
||||
cat > debian/changelog << EOF
|
||||
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
||||
|
||||
* Initial release
|
||||
* Debian Image Builder Frontend with Cockpit integration
|
||||
* React-based web interface for image management
|
||||
|
||||
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
||||
EOF
|
||||
|
||||
cat > debian/compat << EOF
|
||||
13
|
||||
EOF
|
||||
|
||||
chmod +x debian/rules
|
||||
|
||||
- name: Build Debian package
|
||||
run: |
|
||||
dpkg-buildpackage -us -uc -b
|
||||
ls -la ../*.deb
|
||||
|
||||
- name: Upload Debian package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debian-image-builder-frontend-deb
|
||||
path: ../*.deb
|
||||
retention-days: 30
|
||||
|
||||
cockpit-integration:
|
||||
name: Test Cockpit Integration
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:18-bullseye
|
||||
needs: build-and-test
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Test cockpit integration
|
||||
run: |
|
||||
echo "Testing Cockpit integration..."
|
||||
if [ -d cockpit ]; then
|
||||
echo "Cockpit directory found:"
|
||||
ls -la cockpit/
|
||||
else
|
||||
echo "No cockpit directory found"
|
||||
fi
|
||||
|
||||
if [ -f package.json ]; then
|
||||
echo "Package.json scripts:"
|
||||
npm run
|
||||
fi
|
||||
257
.forgejo/workflows/ci.yml.disabled
Normal file
257
.forgejo/workflows/ci.yml.disabled
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
---
|
||||
name: Debian Image Builder Frontend CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18"
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build and Test Frontend
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:18-bullseye
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
git \
|
||||
ca-certificates \
|
||||
python3
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: |
|
||||
npm ci
|
||||
npm run build || echo "Build script not found"
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
if [ -f package.json ] && npm run test; then
|
||||
npm test
|
||||
else
|
||||
echo "No test script found, skipping tests"
|
||||
fi
|
||||
|
||||
- name: Run linting
|
||||
run: |
|
||||
if [ -f package.json ] && npm run lint; then
|
||||
npm run lint
|
||||
else
|
||||
echo "No lint script found, skipping linting"
|
||||
fi
|
||||
|
||||
- name: Build production bundle
|
||||
run: |
|
||||
if [ -f package.json ] && npm run build; then
|
||||
npm run build
|
||||
else
|
||||
echo "No build script found"
|
||||
fi
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-build
|
||||
path: |
|
||||
dist/
|
||||
build/
|
||||
retention-days: 30
|
||||
|
||||
package:
|
||||
name: Package Frontend
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:18-bullseye
|
||||
needs: build-and-test
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
devscripts \
|
||||
debhelper \
|
||||
git \
|
||||
ca-certificates \
|
||||
python3
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build production bundle
|
||||
run: |
|
||||
if [ -f package.json ] && npm run build; then
|
||||
npm run build
|
||||
else
|
||||
echo "No build script found"
|
||||
fi
|
||||
|
||||
- name: Create debian directory
|
||||
run: |
|
||||
mkdir -p debian
|
||||
cat > debian/control << EOF
|
||||
Source: debian-image-builder-frontend
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Debian Forge Team <team@debian-forge.org>
|
||||
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
||||
Standards-Version: 4.6.2
|
||||
|
||||
Package: debian-image-builder-frontend
|
||||
Architecture: all
|
||||
Depends: \${misc:Depends}, nodejs, nginx
|
||||
Description: Debian Image Builder Frontend
|
||||
Web-based frontend for Debian Image Builder with Cockpit integration.
|
||||
Provides a user interface for managing image builds, blueprints,
|
||||
and system configurations through a modern React application.
|
||||
EOF
|
||||
|
||||
cat > debian/rules << EOF
|
||||
#!/usr/bin/make -f
|
||||
%:
|
||||
dh \$@
|
||||
|
||||
override_dh_auto_install:
|
||||
dh_auto_install
|
||||
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
||||
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
||||
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
||||
|
||||
# Copy built frontend files
|
||||
if [ -d dist ]; then
|
||||
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||
elif [ -d build ]; then
|
||||
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||
fi
|
||||
|
||||
# Copy source files for development
|
||||
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
||||
|
||||
# Create nginx configuration
|
||||
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/debian-image-builder-frontend;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8080/;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
}
|
||||
}
|
||||
NGINX_EOF
|
||||
|
||||
# Create cockpit manifest
|
||||
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
||||
{
|
||||
"version": 1,
|
||||
"manifest": {
|
||||
"name": "debian-image-builder",
|
||||
"version": "1.0.0",
|
||||
"title": "Debian Image Builder",
|
||||
"description": "Build and manage Debian atomic images",
|
||||
"url": "/usr/share/debian-image-builder-frontend",
|
||||
"icon": "debian-logo",
|
||||
"requires": {
|
||||
"cockpit": ">= 200"
|
||||
}
|
||||
}
|
||||
}
|
||||
COCKPIT_EOF
|
||||
EOF
|
||||
|
||||
cat > debian/changelog << EOF
|
||||
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
||||
|
||||
* Initial release
|
||||
* Debian Image Builder Frontend with Cockpit integration
|
||||
* React-based web interface for image management
|
||||
|
||||
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
||||
EOF
|
||||
|
||||
cat > debian/compat << EOF
|
||||
13
|
||||
EOF
|
||||
|
||||
chmod +x debian/rules
|
||||
|
||||
- name: Build Debian package
|
||||
run: |
|
||||
dpkg-buildpackage -us -uc -b
|
||||
ls -la ../*.deb
|
||||
|
||||
- name: Upload Debian package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debian-image-builder-frontend-deb
|
||||
path: ../*.deb
|
||||
retention-days: 30
|
||||
|
||||
cockpit-integration:
|
||||
name: Test Cockpit Integration
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:18-bullseye
|
||||
needs: build-and-test
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Test cockpit integration
|
||||
run: |
|
||||
echo "Testing Cockpit integration..."
|
||||
if [ -d cockpit ]; then
|
||||
echo "Cockpit directory found:"
|
||||
ls -la cockpit/
|
||||
else
|
||||
echo "No cockpit directory found"
|
||||
fi
|
||||
|
||||
if [ -f package.json ]; then
|
||||
echo "Package.json scripts:"
|
||||
npm run
|
||||
fi
|
||||
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: 5
|
||||
open-pull-requests-limit: 3
|
||||
rebase-strategy: "auto"
|
||||
ignore:
|
||||
- dependency-name: "@playwright/test"
|
||||
|
|
|
|||
86
.github/workflows/dev-checks.yml
vendored
86
.github/workflows/dev-checks.yml
vendored
|
|
@ -5,35 +5,79 @@ on:
|
|||
branches: [ "main" ]
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
dev-check:
|
||||
build:
|
||||
name: Build Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js 20
|
||||
- name: Use Node.js 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
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
|
||||
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
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Check for manual changes to API
|
||||
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
|
||||
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
|
||||
|
|
|
|||
9
.github/workflows/playwright.yml
vendored
9
.github/workflows/playwright.yml
vendored
|
|
@ -4,6 +4,13 @@ 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:
|
||||
|
|
@ -30,7 +37,7 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
|
||||
- name: Install front-end dependencies
|
||||
|
|
|
|||
1
.github/workflows/pr_best_practices.yml
vendored
1
.github/workflows/pr_best_practices.yml
vendored
|
|
@ -6,6 +6,7 @@ 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 20
|
||||
- name: Use Node.js 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
|
|||
1
.github/workflows/stale-cleanup.yml
vendored
1
.github/workflows/stale-cleanup.yml
vendored
|
|
@ -8,6 +8,7 @@ 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
Normal file
51
.github/workflows/unit-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
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
Normal file
51
.github/workflows/update-apis.yml
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# 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,3 +48,4 @@ rpmbuild
|
|||
/blob-report/
|
||||
/playwright/.cache/
|
||||
.env
|
||||
.auth
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ test:
|
|||
- RUNNER:
|
||||
- aws/fedora-41-x86_64
|
||||
- aws/fedora-42-x86_64
|
||||
- aws/rhel-9.6-nightly-x86_64
|
||||
- aws/rhel-10.0-nightly-x86_64
|
||||
- aws/rhel-10.1-nightly-x86_64
|
||||
INTERNAL_NETWORK: ["true"]
|
||||
|
||||
finish:
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": false,
|
||||
"jsxSingleQuote": false,
|
||||
"bracketSpacing": true,
|
||||
"tsxSingleQuote": true,
|
||||
"tsSingleQuote": true
|
||||
}
|
||||
|
|
@ -7,9 +7,8 @@ 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"
|
||||
creationTimestamp: null
|
||||
pipelinesascode.tekton.dev/on-cel-expression: (event == "pull_request" && target_branch == "main") || (event == "push" && target_branch.startsWith("gh-readonly-queue/main/"))
|
||||
creationTimestamp:
|
||||
labels:
|
||||
appstudio.openshift.io/application: insights-image-builder
|
||||
appstudio.openshift.io/component: image-builder-frontend
|
||||
|
|
@ -46,7 +45,7 @@ spec:
|
|||
- name: name
|
||||
value: show-sbom
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:002f7c8c1d2f9e09904035da414aba1188ae091df0ea9532cd997be05e73d594
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:beb0616db051952b4b861dd8c3e00fa1c0eccbd926feddf71194d3bb3ace9ce7
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -65,7 +64,7 @@ spec:
|
|||
- name: name
|
||||
value: summary
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:76075b709fa06ed824cbc84f41448b397b85bfde1cf9809395ba6d286f5b7cbd
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:3f6e8513cbd70f0416eb6c6f2766973a754778526125ff33d8e3633def917091
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -84,13 +83,11 @@ 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"
|
||||
|
|
@ -110,8 +107,7 @@ 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.
|
||||
|
|
@ -156,7 +152,7 @@ spec:
|
|||
- name: name
|
||||
value: init
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:66e90d31e1386bf516fb548cd3e3f0082b5d0234b8b90dbf9e0d4684b70dbe1a
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:08e18a4dc5f947c1d20e8353a19d013144bea87b72f67236b165dd4778523951
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -173,7 +169,7 @@ spec:
|
|||
- name: name
|
||||
value: git-clone
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:3ced9a6b9d8520773d3ffbf062190515a362ecda11e72f56e38e4dd980294b57
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:7939000e2f92fc8b5d2c4ee4ba9000433c5aa7700d2915a1d4763853d5fd1fd4
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -198,7 +194,7 @@ spec:
|
|||
- name: name
|
||||
value: prefetch-dependencies
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:8c649b82a9d228018e5a5d9b844df9fd1db63db33c9b5034586af3a766378de7
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -242,7 +238,7 @@ spec:
|
|||
- name: name
|
||||
value: buildah
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:1799515338c544f6917044398777714c9e0691895231a9d7f456dca75c6f4b65
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -274,7 +270,7 @@ spec:
|
|||
- name: name
|
||||
value: build-image-index
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:9c95b1fe17db091ae364344ba2006af46648e08486eef1f6fe1b9e3f10866875
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:72f77a8c62f9d6f69ab5c35170839e4b190026e6cc3d7d4ceafa7033fc30ad7b
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -286,7 +282,9 @@ spec:
|
|||
- name: build-source-image
|
||||
params:
|
||||
- name: BINARY_IMAGE
|
||||
value: $(params.output-image)
|
||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||
- name: BINARY_IMAGE_DIGEST
|
||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
||||
runAfter:
|
||||
- build-image-index
|
||||
taskRef:
|
||||
|
|
@ -294,7 +292,7 @@ spec:
|
|||
- name: name
|
||||
value: source-build
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.2@sha256:6077f293bd810c2642200f6c531d938a917201861535b7f720d37f7ed7c5d88d
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.3@sha256:96ed9431854ecf9805407dca77b063abdf7aba1b3b9d1925a5c6145c6b7e95fd
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -323,7 +321,7 @@ spec:
|
|||
- name: name
|
||||
value: sast-shell-check
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:18d594df21cb92cbc409065b25a863492ea7209e2a34045ced69a24a68ca41d8
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:4a63982791a1a68f560c486f524ef5b9fdbeee0c16fe079eee3181a2cfd1c1bf
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -339,6 +337,8 @@ 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.2@sha256:4d5bf6549e42184e462ab7ccfba0153954c65214aa82f319a3215e94e068cded
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check:0.3@sha256:bec18fa5e82e801c3f267f29bf94535a5024e72476f2b27cca7271d506abb5ad
|
||||
- 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:ecd33669676b3a193ff4c2c6223cb912cc1b0cf5cc36e080eaec7718500272cf
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:1d07d16810c26713f3d875083924d93697900147364360587ccb5a63f2c31012
|
||||
- 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:68a8fe28527c4469243119a449e2b3a6655f2acac589c069ea6433242da8ed4d
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:893ffa3ce26b061e21bb4d8db9ef7ed4ddd4044fe7aa5451ef391034da3ff759
|
||||
- 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:302828e9d7abc72b8a44fb2b9be068f86c982d8e5f4550b8bf654571d6361ee8
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
||||
- 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:d7bdc1b08b384f5db323c88ccd3aab1ea58db1d401ff2b2338f4b984eec44e1b
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:351f2dce893159b703e9b6d430a2450b3df9967cb9bd3adb46852df8ccfe4c0d
|
||||
- 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.2@sha256:386c8c3395b44f6eb927dbad72382808b0ae42008f183064ca77cb4cad998442
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:cce2dfcc5bd6e91ee54aacdadad523b013eeae5cdaa7f6a4624b8cbcc040f439
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -471,8 +471,10 @@ spec:
|
|||
- "false"
|
||||
- name: apply-tags
|
||||
params:
|
||||
- name: IMAGE
|
||||
- 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:
|
||||
|
|
@ -480,7 +482,7 @@ spec:
|
|||
- name: name
|
||||
value: apply-tags
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.1@sha256:1c6f673fe100a49f58aaef62580c8adf0c397790964f4e7bac7fcd3f4d07c92e
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.2@sha256:70881c97a4c51ee1f4d023fa1110e0bdfcfd2f51d9a261fa543c3862b9a4eee9
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -501,7 +503,7 @@ spec:
|
|||
- name: name
|
||||
value: push-dockerfile
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:c82189e5d331e489cff99f0399f133fd3fad08921bea86747dfa379d1b5c748d
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -521,7 +523,7 @@ spec:
|
|||
- name: name
|
||||
value: rpms-signature-scan
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:80a4562d5f86eb6812f00d4e30e94c1ad27ec937735dc29f5a63e9335676b3dc
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:1b6c20ab3dbfb0972803d3ebcb2fa72642e59400c77bd66dfd82028bdd09e120
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -542,7 +544,7 @@ spec:
|
|||
- name: workspace
|
||||
volumeClaimTemplate:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
creationTimestamp:
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@ 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: null
|
||||
pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch == "main"
|
||||
creationTimestamp:
|
||||
labels:
|
||||
appstudio.openshift.io/application: insights-image-builder
|
||||
appstudio.openshift.io/component: image-builder-frontend
|
||||
|
|
@ -43,7 +42,7 @@ spec:
|
|||
- name: name
|
||||
value: show-sbom
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:002f7c8c1d2f9e09904035da414aba1188ae091df0ea9532cd997be05e73d594
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:beb0616db051952b4b861dd8c3e00fa1c0eccbd926feddf71194d3bb3ace9ce7
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -62,7 +61,7 @@ spec:
|
|||
- name: name
|
||||
value: summary
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:76075b709fa06ed824cbc84f41448b397b85bfde1cf9809395ba6d286f5b7cbd
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:3f6e8513cbd70f0416eb6c6f2766973a754778526125ff33d8e3633def917091
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -81,13 +80,11 @@ 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,8 +104,7 @@ 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.
|
||||
|
|
@ -153,7 +149,7 @@ spec:
|
|||
- name: name
|
||||
value: init
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:66e90d31e1386bf516fb548cd3e3f0082b5d0234b8b90dbf9e0d4684b70dbe1a
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:08e18a4dc5f947c1d20e8353a19d013144bea87b72f67236b165dd4778523951
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -170,7 +166,7 @@ spec:
|
|||
- name: name
|
||||
value: git-clone
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:3ced9a6b9d8520773d3ffbf062190515a362ecda11e72f56e38e4dd980294b57
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:7939000e2f92fc8b5d2c4ee4ba9000433c5aa7700d2915a1d4763853d5fd1fd4
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -195,7 +191,7 @@ spec:
|
|||
- name: name
|
||||
value: prefetch-dependencies
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:8c649b82a9d228018e5a5d9b844df9fd1db63db33c9b5034586af3a766378de7
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -239,7 +235,7 @@ spec:
|
|||
- name: name
|
||||
value: buildah
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:1799515338c544f6917044398777714c9e0691895231a9d7f456dca75c6f4b65
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -271,7 +267,7 @@ spec:
|
|||
- name: name
|
||||
value: build-image-index
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:9c95b1fe17db091ae364344ba2006af46648e08486eef1f6fe1b9e3f10866875
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:72f77a8c62f9d6f69ab5c35170839e4b190026e6cc3d7d4ceafa7033fc30ad7b
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -283,7 +279,9 @@ spec:
|
|||
- name: build-source-image
|
||||
params:
|
||||
- name: BINARY_IMAGE
|
||||
value: $(params.output-image)
|
||||
value: $(tasks.build-image-index.results.IMAGE_URL)
|
||||
- name: BINARY_IMAGE_DIGEST
|
||||
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
|
||||
runAfter:
|
||||
- build-image-index
|
||||
taskRef:
|
||||
|
|
@ -291,7 +289,7 @@ spec:
|
|||
- name: name
|
||||
value: source-build
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.2@sha256:6077f293bd810c2642200f6c531d938a917201861535b7f720d37f7ed7c5d88d
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.3@sha256:96ed9431854ecf9805407dca77b063abdf7aba1b3b9d1925a5c6145c6b7e95fd
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -320,7 +318,7 @@ spec:
|
|||
- name: name
|
||||
value: sast-shell-check
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:18d594df21cb92cbc409065b25a863492ea7209e2a34045ced69a24a68ca41d8
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:4a63982791a1a68f560c486f524ef5b9fdbeee0c16fe079eee3181a2cfd1c1bf
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -336,6 +334,8 @@ 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.2@sha256:4d5bf6549e42184e462ab7ccfba0153954c65214aa82f319a3215e94e068cded
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check:0.3@sha256:bec18fa5e82e801c3f267f29bf94535a5024e72476f2b27cca7271d506abb5ad
|
||||
- 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:ecd33669676b3a193ff4c2c6223cb912cc1b0cf5cc36e080eaec7718500272cf
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:1d07d16810c26713f3d875083924d93697900147364360587ccb5a63f2c31012
|
||||
- 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:68a8fe28527c4469243119a449e2b3a6655f2acac589c069ea6433242da8ed4d
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:893ffa3ce26b061e21bb4d8db9ef7ed4ddd4044fe7aa5451ef391034da3ff759
|
||||
- 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:302828e9d7abc72b8a44fb2b9be068f86c982d8e5f4550b8bf654571d6361ee8
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
|
||||
- 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:d7bdc1b08b384f5db323c88ccd3aab1ea58db1d401ff2b2338f4b984eec44e1b
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:351f2dce893159b703e9b6d430a2450b3df9967cb9bd3adb46852df8ccfe4c0d
|
||||
- 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.2@sha256:386c8c3395b44f6eb927dbad72382808b0ae42008f183064ca77cb4cad998442
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:cce2dfcc5bd6e91ee54aacdadad523b013eeae5cdaa7f6a4624b8cbcc040f439
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -468,8 +468,10 @@ spec:
|
|||
- "false"
|
||||
- name: apply-tags
|
||||
params:
|
||||
- name: IMAGE
|
||||
- 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:
|
||||
|
|
@ -477,7 +479,7 @@ spec:
|
|||
- name: name
|
||||
value: apply-tags
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.1@sha256:1c6f673fe100a49f58aaef62580c8adf0c397790964f4e7bac7fcd3f4d07c92e
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.2@sha256:70881c97a4c51ee1f4d023fa1110e0bdfcfd2f51d9a261fa543c3862b9a4eee9
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -498,7 +500,7 @@ spec:
|
|||
- name: name
|
||||
value: push-dockerfile
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:c82189e5d331e489cff99f0399f133fd3fad08921bea86747dfa379d1b5c748d
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -518,7 +520,7 @@ spec:
|
|||
- name: name
|
||||
value: rpms-signature-scan
|
||||
- name: bundle
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:80a4562d5f86eb6812f00d4e30e94c1ad27ec937735dc29f5a63e9335676b3dc
|
||||
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:1b6c20ab3dbfb0972803d3ebcb2fa72642e59400c77bd66dfd82028bdd09e120
|
||||
- name: kind
|
||||
value: task
|
||||
resolver: bundles
|
||||
|
|
@ -539,7 +541,7 @@ spec:
|
|||
- name: workspace
|
||||
volumeClaimTemplate:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
creationTimestamp:
|
||||
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 = b0e82161b4afcb9f0a6fddd8ff94380e983b2238
|
||||
COCKPIT_REPO_COMMIT = a70142a7a6f9c4e78e71f3c4ec738b6db2fbb04f
|
||||
COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
|
||||
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
|
||||
|
||||
|
|
|
|||
325
README.md
325
README.md
|
|
@ -19,16 +19,20 @@ Frontend code for Image Builder.
|
|||
## Table of Contents
|
||||
1. [How to build and run image-builder-frontend](#frontend-development)
|
||||
1. [Frontend Development](#frontend-development)
|
||||
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)
|
||||
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)
|
||||
|
||||
## 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
|
||||
|
|
@ -39,7 +43,7 @@ worrying if a feature from stage has been released yet.
|
|||
|
||||
#### Nodejs and npm version
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
#### Webpack proxy
|
||||
|
||||
|
|
@ -69,165 +73,21 @@ 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`
|
||||
|
||||
#### Insights proxy (deprecated)
|
||||
### Image builder as Cockpit plugin
|
||||
|
||||
1. Clone the insights proxy: https://github.com/RedHatInsights/insights-proxy
|
||||
> [!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.
|
||||
|
||||
2. Setting up the proxy
|
||||
#### Cockpit setup
|
||||
To install and setup Cockpit follow guide at: https://cockpit-project.org/running.html
|
||||
|
||||
Choose a runner (podman or docker), and point the SPANDX_CONFIG variable to
|
||||
`profile/local-frontend.js` included in image-builder-frontend.
|
||||
#### 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/
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
3. Starting up image-builder-frontend
|
||||
|
||||
In the image-builder-frontend checkout directory
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### 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.
|
||||
|
||||
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
|
||||
|
||||
For a hypothetical API called foobar
|
||||
|
||||
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
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { FOOBAR_API } from '../constants';
|
||||
|
||||
// initialize an empty api service that we'll inject endpoints into later as needed
|
||||
export const emptyFoobarApi = createApi({
|
||||
reducerPath: 'foobarApi',
|
||||
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + FOO_BAR }),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
```
|
||||
|
||||
3. Declare new constant `FOOBAR_API` with the API url in `src/constants.ts`
|
||||
|
||||
```typescript
|
||||
export const FOOBAR_API = 'api/foobar/v1'
|
||||
```
|
||||
|
||||
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: '../schema/foobar.json',
|
||||
apiFile: '../../src/store/emptyFoobarApi.ts',
|
||||
apiImport: 'emptyEdgeApi',
|
||||
outputFile: '../../src/store/foobarApi.ts',
|
||||
exportName: 'foobarApi',
|
||||
hooks: true,
|
||||
filterEndpoints: ['getFoo', 'getBar', 'getFoobar'],
|
||||
};
|
||||
```
|
||||
|
||||
5. Update the `api.sh` script by adding a new line for npx to generate the code:
|
||||
|
||||
```bash
|
||||
npx @rtk-query/codegen-openapi ./api/config/foobar.ts &
|
||||
```
|
||||
|
||||
|
||||
6. Update the `.eslintignore` file by adding a new line for the generated code:
|
||||
|
||||
```
|
||||
foobarApi.ts
|
||||
```
|
||||
|
||||
7. run api generation
|
||||
|
||||
```bash
|
||||
npm run api
|
||||
```
|
||||
|
||||
And voilà!
|
||||
|
||||
##### 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
|
||||
|
||||
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
|
||||
you can ask on the slack channel https://redhat-internal.slack.com/archives/C023YSA47A4 for a merge if your MR stays unchecked for a little while.
|
||||
|
||||
Then connect to the following platforms:
|
||||
* https://insights-stage.unleash.devshift.net/ for stage
|
||||
* https://insights.unleash.devshift.net prod
|
||||
|
||||
Once you have a toggle to work with, on the frontend code there's just need to
|
||||
import the `useFlag` hook and to use it. You can get some inspiration from
|
||||
existing flags:
|
||||
|
||||
https://github.com/RedHatInsights/image-builder-frontend/blob/c84b493eba82ce83a7844943943d91112ffe8322/src/Components/ImagesTable/ImageLink.js#L99
|
||||
|
||||
##### 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
|
||||
|
||||
If the two possible code path accessible via the toggles are defined in the code
|
||||
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
|
||||
|
||||
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
|
||||
#### Scripts for local development of image builder plugin
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -238,11 +98,11 @@ The symbolic link allows Cockpit to serve the frontend assets from your local de
|
|||
making it easier to test changes in real-time without deploying to a remote server.
|
||||
|
||||
```bash
|
||||
make cockpit/devel-install
|
||||
make cockpit/build
|
||||
```
|
||||
|
||||
```bash
|
||||
make cockpit/build
|
||||
make cockpit/devel-install
|
||||
```
|
||||
|
||||
To uninstall and remove the symbolic link, run the following command:
|
||||
|
|
@ -258,12 +118,122 @@ For convenience, you can run the following to combine all three steps:
|
|||
make cockpit/devel
|
||||
```
|
||||
|
||||
### Backend Development
|
||||
|
||||
To develop both the frontend and the backend you can again use the proxy to run both the
|
||||
frontend and backend locally against the chrome at cloud.redhat.com. For instructions
|
||||
see the [osbuild-getting-started project](https://github.com/osbuild/osbuild-getting-started).
|
||||
|
||||
## API endpoints
|
||||
|
||||
API slice definitions are programmatically generated using the [@rtk-query/codegen-openapi](https://redux-toolkit.js.org/rtk-query/usage/code-generation) package.
|
||||
|
||||
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`.
|
||||
|
||||
### Add a new API schema
|
||||
|
||||
For a hypothetical API called foobar
|
||||
|
||||
1. Create a new "empty" API file under `src/store/emptyFoobarApi.ts` that has following
|
||||
content:
|
||||
|
||||
```typescript
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { FOOBAR_API } from '../constants';
|
||||
|
||||
// initialize an empty api service that we'll inject endpoints into later as needed
|
||||
export const emptyFoobarApi = createApi({
|
||||
reducerPath: 'foobarApi',
|
||||
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + FOO_BAR }),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
```
|
||||
|
||||
2. 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:
|
||||
|
||||
```typescript
|
||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile: 'URL_TO_THE_OPENAPI_SCHEMA',
|
||||
apiFile: '../../src/store/emptyFoobarApi.ts',
|
||||
apiImport: 'emptyContentSourcesApi',
|
||||
outputFile: '../../src/store/foobarApi.ts',
|
||||
exportName: 'foobarApi',
|
||||
hooks: true,
|
||||
filterEndpoints: ['getFoo', 'getBar', 'getFoobar'],
|
||||
};
|
||||
```
|
||||
|
||||
4. Update the `eslint.config.js` file by adding the generated code path to the ignores array:
|
||||
|
||||
```
|
||||
ignores: [
|
||||
<other ignored files>,
|
||||
'**/foobarApi.ts',
|
||||
]
|
||||
```
|
||||
|
||||
5. run api generation
|
||||
|
||||
```bash
|
||||
npm run api
|
||||
```
|
||||
|
||||
And voilà!
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
you can ask on the slack channel https://redhat-internal.slack.com/archives/C023YSA47A4 for a merge if your MR stays unchecked for a little while.
|
||||
|
||||
Then connect to the following platforms:
|
||||
* https://insights-stage.unleash.devshift.net/ for stage
|
||||
* https://insights.unleash.devshift.net prod
|
||||
|
||||
Once you have a toggle to work with, on the frontend code there's just need to
|
||||
import the `useFlag` hook and to use it. You can get some inspiration from
|
||||
existing flags:
|
||||
|
||||
https://github.com/RedHatInsights/image-builder-frontend/blob/c84b493eba82ce83a7844943943d91112ffe8322/src/Components/ImagesTable/ImageLink.js#L99
|
||||
|
||||
### 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
|
||||
|
||||
If the two possible code path accessible via the toggles are defined in the code
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## File Structure
|
||||
### 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 |
|
||||
|
|
@ -272,8 +242,19 @@ make cockpit/devel
|
|||
|
||||
## Style Guidelines
|
||||
|
||||
This project uses eslint's recommended styling guidelines. These rules can be found here:
|
||||
https://eslint.org/docs/rules/
|
||||
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`
|
||||
|
||||
To run the linter, use:
|
||||
```bash
|
||||
|
|
@ -282,16 +263,10 @@ npm run lint
|
|||
|
||||
Any errors that can be fixed automatically, can be corrected by running:
|
||||
```bash
|
||||
npm run lint --fix
|
||||
npm run lint:js:fix
|
||||
```
|
||||
|
||||
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
|
||||
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).
|
||||
|
||||
## Test Guidelines
|
||||
|
||||
|
|
@ -369,12 +344,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,7 +5,6 @@ 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: '../schema/compliance.json',
|
||||
schemaFile: 'https://console.redhat.com/api/compliance/v2/openapi.json',
|
||||
apiFile: '../../src/store/service/emptyComplianceApi.ts',
|
||||
apiImport: 'emptyComplianceApi',
|
||||
outputFile: '../../src/store/service/complianceApi.ts',
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile: '../schema/composerCloudApi.v2.yaml',
|
||||
schemaFile:
|
||||
'https://raw.githubusercontent.com/osbuild/osbuild-composer/main/internal/cloudapi/v2/openapi.v2.yml',
|
||||
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: '../schema/contentSources.json',
|
||||
schemaFile: 'https://console.redhat.com/api/content-sources/v1/openapi.json',
|
||||
apiFile: '../../src/store/service/emptyContentSourcesApi.ts',
|
||||
apiImport: 'emptyContentSourcesApi',
|
||||
outputFile: '../../src/store/service/contentSourcesApi.ts',
|
||||
|
|
@ -12,6 +12,7 @@ const config: ConfigFile = {
|
|||
'createRepository',
|
||||
'listRepositories',
|
||||
'listRepositoriesRpms',
|
||||
'listRepositoryParameters',
|
||||
'searchRpm',
|
||||
'searchPackageGroup',
|
||||
'listFeatures',
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
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,7 +1,8 @@
|
|||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile: '../schema/imageBuilder.yaml',
|
||||
schemaFile:
|
||||
'https://raw.githubusercontent.com/osbuild/image-builder/main/internal/v1/api.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: '../schema/provisioning.json',
|
||||
schemaFile: 'https://console.redhat.com/api/provisioning/v1/openapi.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: '../schema/rhsm.json',
|
||||
schemaFile: 'https://console.redhat.com/api/rhsm/v2/openapi.json',
|
||||
apiFile: '../../src/store/service/emptyRhsmApi.ts',
|
||||
apiImport: 'emptyRhsmApi',
|
||||
outputFile: '../../src/store/service/rhsmApi.ts',
|
||||
|
|
|
|||
10
api/pull.sh
10
api/pull.sh
|
|
@ -1,10 +0,0 @@
|
|||
#!/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
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
5722
api/schema/edge.json
5722
api/schema/edge.json
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
Subproject commit 75adad05c9e22ff84c7d3b43564554a26f55a8a9
|
||||
Subproject commit b496d0a8c1755608bd256a6960869b14a7689d38
|
||||
|
|
@ -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=18
|
||||
export NODE_BUILD_VERSION=22
|
||||
COMMON_BUILDER=https://raw.githubusercontent.com/RedHatInsights/insights-frontend-builder-common/master
|
||||
|
||||
set -exv
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
TODO
|
||||
# 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.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
Name: cockpit-image-builder
|
||||
Version: 69
|
||||
Version: 76
|
||||
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>
|
||||
<link href="main.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script defer src="main.js"></script>
|
||||
</body>
|
||||
<script defer src="main.js"></script>
|
||||
<link href="main.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="ct-page-fill" id="main"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,15 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: { url: false },
|
||||
},
|
||||
'sass-loader',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,183 +0,0 @@
|
|||
---
|
||||
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"
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
FROM node:18
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm ci
|
||||
|
||||
EXPOSE 8002
|
||||
EXPOSE 1337
|
||||
|
||||
CMD [ "npm", "run", "devel" ]
|
||||
174
eslint.config.js
Normal file
174
eslint.config.js
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
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
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
]);
|
||||
4196
package-lock.json
generated
4196
package-lock.json
generated
File diff suppressed because it is too large
Load diff
75
package.json
75
package.json
|
|
@ -8,17 +8,18 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ltd/j-toml": "1.38.0",
|
||||
"@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",
|
||||
"@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",
|
||||
"@reduxjs/toolkit": "2.8.2",
|
||||
"@scalprum/react-core": "0.9.5",
|
||||
"@sentry/webpack-plugin": "3.5.0",
|
||||
"@unleash/proxy-client-react": "5.0.0",
|
||||
"@sentry/webpack-plugin": "4.1.1",
|
||||
"@unleash/proxy-client-react": "5.0.1",
|
||||
"classnames": "2.5.1",
|
||||
"jwt-decode": "4.0.0",
|
||||
"lodash": "4.17.21",
|
||||
|
|
@ -30,68 +31,72 @@
|
|||
"redux-promise-middleware": "6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.10",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/core": "7.28.0",
|
||||
"@babel/preset-env": "7.28.0",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@babel/preset-typescript": "7.27.0",
|
||||
"@currents/playwright": "1.13.2",
|
||||
"@patternfly/react-icons": "6.1.0",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@currents/playwright": "1.15.3",
|
||||
"@eslint/js": "9.32.0",
|
||||
"@patternfly/react-icons": "6.3.1",
|
||||
"@playwright/test": "1.51.1",
|
||||
"@redhat-cloud-services/eslint-config-redhat-cloud-services": "2.0.12",
|
||||
"@redhat-cloud-services/eslint-config-redhat-cloud-services": "3.0.0",
|
||||
"@redhat-cloud-services/frontend-components-config": "6.3.8",
|
||||
"@redhat-cloud-services/tsc-transform-imports": "1.0.24",
|
||||
"@redhat-cloud-services/tsc-transform-imports": "1.0.25",
|
||||
"@rtk-query/codegen-openapi": "2.0.0",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "6.6.4",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/node": "22.15.1",
|
||||
"@types/node": "24.3.0",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-redux": "7.1.34",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||
"@typescript-eslint/parser": "8.32.1",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"@vitest/coverage-v8": "3.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"babel-loader": "10.0.0",
|
||||
"chart.js": "4.4.9",
|
||||
"chart.js": "4.5.0",
|
||||
"chartjs-adapter-moment": "1.0.1",
|
||||
"chartjs-plugin-annotation": "3.1.0",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"css-loader": "7.1.2",
|
||||
"eslint": "8.57.1",
|
||||
"eslint": "9.33.0",
|
||||
"eslint-plugin-disable-autofix": "5.0.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jest-dom": "5.5.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-playwright": "2.2.0",
|
||||
"eslint-plugin-playwright": "2.2.2",
|
||||
"eslint-plugin-prettier": "5.5.4",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-redux": "4.2.2",
|
||||
"eslint-plugin-testing-library": "7.2.2",
|
||||
"eslint-plugin-testing-library": "7.6.6",
|
||||
"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.7.5",
|
||||
"msw": "2.10.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.88.0",
|
||||
"sass": "1.90.0",
|
||||
"sass-loader": "16.0.5",
|
||||
"stylelint": "16.18.0",
|
||||
"stylelint-config-recommended-scss": "14.1.0",
|
||||
"stylelint": "16.23.1",
|
||||
"stylelint-config-recommended-scss": "16.0.0",
|
||||
"ts-node": "10.9.2",
|
||||
"ts-patch": "3.3.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.40.0",
|
||||
"uuid": "11.1.0",
|
||||
"vitest": "3.1.2",
|
||||
"vitest": "3.2.4",
|
||||
"vitest-canvas-mock": "0.3.3",
|
||||
"webpack-bundle-analyzer": "4.10.2",
|
||||
"whatwg-fetch": "3.6.20"
|
||||
|
|
@ -112,9 +117,7 @@
|
|||
"test:cockpit": "src/test/cockpit-tests.sh",
|
||||
"build": "fec build",
|
||||
"build:cockpit": "webpack --config cockpit/webpack.config.ts",
|
||||
"api": "npm-run-all api:pull api:generate",
|
||||
"api:generate": "bash api/codegen.sh",
|
||||
"api:pull": "bash api/pull.sh",
|
||||
"api": "bash api/codegen.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,6 +16,15 @@ 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
|
||||
|
|
@ -24,7 +33,6 @@ jobs:
|
|||
- centos-stream-10
|
||||
- centos-stream-10-aarch64
|
||||
- fedora-all
|
||||
- fedora-all-aarch64
|
||||
|
||||
- job: copr_build
|
||||
trigger: commit
|
||||
|
|
|
|||
14
plans/all.fmf
Normal file
14
plans/all.fmf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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,9 +34,14 @@ export default defineConfig({
|
|||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
projects: [
|
||||
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: '.auth/user.json',
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
214
playwright/Customizations/AAP.spec.ts
Normal file
214
playwright/Customizations/AAP.spec.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} from '../helpers/navHelpers';
|
||||
import {
|
||||
createBlueprint,
|
||||
deleteBlueprint,
|
||||
exportBlueprint,
|
||||
fillInDetails,
|
||||
fillInImageOutputGuest,
|
||||
importBlueprint,
|
||||
registerLater,
|
||||
} from '../helpers/wizardHelpers';
|
||||
|
||||
const validCallbackUrl =
|
||||
'https://controller.url/api/controller/v2/job_templates/9/callback/';
|
||||
const validHttpCallbackUrl =
|
||||
'http://controller.url/api/controller/v2/job_templates/9/callback/';
|
||||
const validHostConfigKey = 'hostconfigkey';
|
||||
const validCertificate = `-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJAOEzx5ezZ9EIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAklOMQswCQYDVQQIDAJLUjEMMAoGA1UEBwwDS1JHMRAwDgYDVQQKDAdUZXN0
|
||||
IENBMB4XDTI1MDUxNTEyMDAwMFoXDTI2MDUxNTEyMDAwMFowRTELMAkGA1UEBhMC
|
||||
SU4xCzAJBgNVBAgMAktSMQwwCgYDVQQHDANSR0sxEDAOBgNVBAoMB1Rlc3QgQ0Ew
|
||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+R4gfN5pyJQo5qBTTtN+7
|
||||
eE9CSXZJ8SVVaE3U54IgqQoqsSoBY5QtExy7v5C6l6mW4E6dzK/JecmvTTO/BvlG
|
||||
A5k2hxB6bOQxtxYwfgElH+RFWN9P4xxhtEiQgHoG1rDfnXuDJk1U3YEkCQELUebz
|
||||
fF3EIDU1yR0Sz2bA+Sl2VXe8og1MEZfytq8VZUVltxtn2PfW7zI5gOllBR2sKeUc
|
||||
K6h8HXN7qMgfEvsLIXxTw7fU/zA3ibcxfRCl3m6QhF8hwRh6F9Wtz2s8hCzGegV5
|
||||
z0M39nY7X8C3GZQ4Ly8v8DdY+FbEix7K3SSBRbWtdPfAHRFlX9Er2Wf8DAr7O2hH
|
||||
AgMBAAGjUDBOMB0GA1UdDgQWBBTXXz2eIDgK+BhzDUAGzptn0OMcpDAfBgNVHSME
|
||||
GDAWgBTXXz2eIDgK+BhzDUAGzptn0OMcpDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQAoUgY4jsuBMB3el9cc7JS2rcOhhJzn47Hj2UANfJq52g5lbjo7
|
||||
XDc7Wb3VDcV+1LzjdzayT1qO1WzHb6FDPW9L9f6h4s8lj6MvJ+xhOWgD11srdIt3
|
||||
vbQaQW4zDfeVRcKXzqbcUX8BLXAdzJPqVwZ+Z4EDjYrJ7lF9k+IqfZm0MsYX7el9
|
||||
kvdRHbLuF4Q0sZ05CXMFkhM0Ulhu4MZ+1FcsQa7nWfZzTmbjHOuWJPB4z5WwrB7z
|
||||
U8YYvWJ3qxToWGbATqJxkRKGGqLrNrmwcfzgPqkpuCRYi0Kky6gJ1RvL+DRopY9x
|
||||
uD+ckf3oH2wYAB6RpPRMkfVxe7lGMvq/yEZ6
|
||||
-----END CERTIFICATE-----`;
|
||||
const invalidCertificate = `-----BEGIN CERTIFICATE-----
|
||||
ThisIs*Not+Valid/Base64==
|
||||
-----END CERTIFICATE-----`;
|
||||
|
||||
test('Create a blueprint with AAP registration customization', async ({
|
||||
page,
|
||||
cleanup,
|
||||
}) => {
|
||||
const blueprintName = 'test-' + uuidv4();
|
||||
|
||||
// Skip entirely in Cockpit/on-premise where AAP customization is unavailable
|
||||
test.skip(!isHosted(), 'AAP customization is not available in the plugin');
|
||||
|
||||
// Delete the blueprint after the run fixture
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
await ensureAuthenticated(page);
|
||||
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
||||
await navigateToOptionalSteps(frame);
|
||||
await registerLater(frame);
|
||||
});
|
||||
|
||||
await test.step('Select and fill the AAP step with valid configuration', async () => {
|
||||
await frame
|
||||
.getByRole('button', { name: 'Ansible Automation Platform' })
|
||||
.click();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'ansible callback url' })
|
||||
.fill(validCallbackUrl);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'host config key' })
|
||||
.fill(validHostConfigKey);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'File upload' })
|
||||
.fill(validCertificate);
|
||||
await expect(frame.getByRole('button', { name: 'Next' })).toBeEnabled();
|
||||
});
|
||||
|
||||
await test.step('Test TLS confirmation checkbox for HTTPS URLs', async () => {
|
||||
// TLS confirmation checkbox should appear for HTTPS URLs
|
||||
await expect(
|
||||
frame.getByRole('checkbox', {
|
||||
name: 'Insecure',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
// Check TLS confirmation and verify CA input is hidden
|
||||
await frame
|
||||
.getByRole('checkbox', {
|
||||
name: 'Insecure',
|
||||
})
|
||||
.check();
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'File upload' }),
|
||||
).toBeHidden();
|
||||
|
||||
await frame
|
||||
.getByRole('checkbox', {
|
||||
name: 'Insecure',
|
||||
})
|
||||
.uncheck();
|
||||
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'File upload' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Test certificate validation', async () => {
|
||||
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'File upload' })
|
||||
.fill(invalidCertificate);
|
||||
await expect(frame.getByText(/Certificate.*is not valid/)).toBeVisible();
|
||||
|
||||
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'File upload' })
|
||||
.fill(validCertificate);
|
||||
|
||||
await expect(frame.getByText('Certificate was uploaded')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Test HTTP URL behavior', async () => {
|
||||
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'ansible callback url' })
|
||||
.fill(validHttpCallbackUrl);
|
||||
|
||||
// TLS confirmation checkbox should NOT appear for HTTP URLs
|
||||
await expect(
|
||||
frame.getByRole('checkbox', {
|
||||
name: 'Insecure',
|
||||
}),
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'File upload' }),
|
||||
).toBeVisible();
|
||||
|
||||
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'ansible callback url' })
|
||||
.fill(validCallbackUrl);
|
||||
});
|
||||
|
||||
await test.step('Complete AAP configuration and proceed to review', async () => {
|
||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||
});
|
||||
|
||||
await test.step('Fill the BP details', async () => {
|
||||
await fillInDetails(frame, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Create BP', async () => {
|
||||
await createBlueprint(frame, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Edit BP and verify AAP configuration persists', async () => {
|
||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
||||
await frame.getByLabel('Revisit Ansible Automation Platform step').click();
|
||||
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'ansible callback url' }),
|
||||
).toHaveValue(validCallbackUrl);
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'host config key' }),
|
||||
).toHaveValue(validHostConfigKey);
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'File upload' }),
|
||||
).toHaveValue(validCertificate);
|
||||
|
||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||
await frame
|
||||
.getByRole('button', { name: 'Save changes to blueprint' })
|
||||
.click();
|
||||
});
|
||||
// This is for hosted service only as these features are not available in cockpit plugin
|
||||
await test.step('Export BP', async (step) => {
|
||||
step.skip(!isHosted(), 'Exporting is not available in the plugin');
|
||||
await exportBlueprint(page, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Import BP', async (step) => {
|
||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
||||
await importBlueprint(page, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Review imported BP', async (step) => {
|
||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
||||
await fillInImageOutputGuest(page);
|
||||
await page
|
||||
.getByRole('button', { name: 'Ansible Automation Platform' })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'ansible callback url' }),
|
||||
).toHaveValue(validCallbackUrl);
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'host config key' }),
|
||||
).toBeEmpty();
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'File upload' }),
|
||||
).toHaveValue(validCertificate);
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
});
|
||||
});
|
||||
230
playwright/Customizations/Filesystem.spec.ts
Normal file
230
playwright/Customizations/Filesystem.spec.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
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,18 +1,22 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} 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 ({
|
||||
|
|
@ -24,8 +28,10 @@ test('Create a blueprint with Firewall customization', async ({
|
|||
// Delete the blueprint after the run fixture
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
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 () => {
|
||||
|
|
@ -57,19 +63,29 @@ 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('Invalid format.').nth(0)).toBeVisible();
|
||||
await expect(
|
||||
frame
|
||||
.getByText(
|
||||
'Expected format: <port/port-name>:<protocol>. Example: 8080:tcp, ssh:tcp',
|
||||
)
|
||||
.nth(0),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Select and incorrectly fill the disabled services in Firewall step', async () => {
|
||||
await frame.getByPlaceholder('Add disabled service').fill('1');
|
||||
await frame.getByRole('button', { name: 'Add disabled service' }).click();
|
||||
await expect(frame.getByText('Invalid format.').nth(1)).toBeVisible();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(0),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Select and incorrectly fill the enabled services in Firewall step', async () => {
|
||||
await frame.getByPlaceholder('Add enabled service').fill('ťčš');
|
||||
await frame.getByRole('button', { name: 'Add enabled service' }).click();
|
||||
await expect(frame.getByText('Invalid format.').nth(2)).toBeVisible();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(1),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Fill the BP details', async () => {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} 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 ({
|
||||
|
|
@ -25,8 +29,10 @@ test('Create a blueprint with Hostname customization', async ({
|
|||
// Delete the blueprint after the run fixture
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
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 () => {
|
||||
|
|
@ -77,7 +83,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();
|
||||
});
|
||||
|
|
|
|||
133
playwright/Customizations/Kernel.spec.ts
Normal file
133
playwright/Customizations/Kernel.spec.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
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,18 +1,22 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} 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 ({
|
||||
|
|
@ -24,8 +28,10 @@ test('Create a blueprint with Locale customization', async ({
|
|||
// Delete the blueprint after the run fixture
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
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 () => {
|
||||
|
|
@ -36,27 +42,45 @@ 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: 'fy_DE.UTF-' }).click();
|
||||
await frame
|
||||
.getByRole('option', { name: 'Western Frisian - Germany (fy_DE.UTF-8)' })
|
||||
.click();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
).toBeEnabled();
|
||||
await frame.getByRole('button', { name: 'Close fy_DE.UTF-' }).click();
|
||||
await frame
|
||||
.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
})
|
||||
.click();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
).toBeHidden();
|
||||
await frame.getByPlaceholder('Select a language').fill('fy');
|
||||
await frame.getByRole('option', { name: 'fy_DE.UTF-' }).click();
|
||||
await frame
|
||||
.getByRole('option', { name: 'Western Frisian - Germany (fy_DE.UTF-8)' })
|
||||
.click();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close fy_DE.UTF-' })
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
).toBeEnabled();
|
||||
await frame.getByPlaceholder('Select a language').fill('aa');
|
||||
await frame.getByRole('option', { name: 'aa_DJ.UTF-' }).click();
|
||||
await frame
|
||||
.getByRole('option', { name: 'aa - Djibouti (aa_DJ.UTF-8)' })
|
||||
.click();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa_DJ.UTF-' })
|
||||
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
|
||||
).toBeEnabled();
|
||||
await frame.getByPlaceholder('Select a language').fill('aa');
|
||||
await expect(
|
||||
frame.getByText('aa_DJ.UTF-8Language already addedaa_ER.UTF-8aa_ET.UTF-')
|
||||
frame.getByText(
|
||||
'aa - Djibouti (aa_DJ.UTF-8)Language already addedaa - Eritrea (aa_ER.UTF-8)aa - Ethiopia (aa_ET.UTF-8)',
|
||||
),
|
||||
).toBeAttached();
|
||||
await frame.getByPlaceholder('Select a language').fill('xxx');
|
||||
await expect(frame.getByText('No results found for')).toBeAttached();
|
||||
|
|
@ -78,15 +102,19 @@ 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 fy_DE.UTF-' })
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
).toBeEnabled();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa_DJ.UTF-' })
|
||||
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
|
||||
).toBeEnabled();
|
||||
await frame.getByPlaceholder('Select a language').fill('aa');
|
||||
await frame.getByRole('option', { name: 'aa_ER.UTF-' }).click();
|
||||
await frame
|
||||
.getByRole('option', { name: 'aa - Eritrea (aa_ER.UTF-8)' })
|
||||
.click();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa_ER.UTF-' })
|
||||
frame.getByRole('button', { name: 'Close aa - Eritrea (aa_ER.UTF-8)' }),
|
||||
).toBeEnabled();
|
||||
await frame.getByRole('button', { name: 'Clear input' }).click();
|
||||
await frame.getByRole('button', { name: 'Menu toggle' }).nth(1).click();
|
||||
|
|
@ -113,16 +141,18 @@ 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 fy_DE.UTF-' })
|
||||
frame.getByRole('button', {
|
||||
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
|
||||
}),
|
||||
).toBeEnabled();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa_DJ.UTF-' })
|
||||
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
|
||||
).toBeEnabled();
|
||||
await expect(
|
||||
frame.getByRole('button', { name: 'Close aa_ER.UTF-' })
|
||||
frame.getByRole('button', { name: 'Close aa - Eritrea (aa_ER.UTF-8)' }),
|
||||
).toBeEnabled();
|
||||
await expect(frame.getByPlaceholder('Select a keyboard')).toHaveValue(
|
||||
'ANSI-dvorak'
|
||||
'ANSI-dvorak',
|
||||
);
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
});
|
||||
|
|
|
|||
189
playwright/Customizations/OpenSCAP.spec.ts
Normal file
189
playwright/Customizations/OpenSCAP.spec.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
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,18 +1,22 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} 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 ({
|
||||
|
|
@ -24,8 +28,10 @@ test('Create a blueprint with Systemd customization', async ({
|
|||
// Delete the blueprint after the run fixture
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
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 () => {
|
||||
|
|
@ -58,15 +64,21 @@ 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('Invalid format.').nth(0)).toBeVisible();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(0),
|
||||
).toBeVisible();
|
||||
|
||||
await frame.getByPlaceholder('Add enabled service').fill('áá');
|
||||
await frame.getByRole('button', { name: 'Add enabled service' }).click();
|
||||
await expect(frame.getByText('Invalid format.').nth(1)).toBeVisible();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(1),
|
||||
).toBeVisible();
|
||||
|
||||
await frame.getByPlaceholder('Add masked service').fill('78');
|
||||
await frame.getByRole('button', { name: 'Add masked service' }).click();
|
||||
await expect(frame.getByText('Invalid format.').nth(2)).toBeVisible();
|
||||
await expect(
|
||||
frame.getByText('Expected format: <service-name>. Example: sshd').nth(2),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Fill the BP details', async () => {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} 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 ({
|
||||
|
|
@ -24,8 +28,10 @@ test('Create a blueprint with Timezone customization', async ({
|
|||
// Delete the blueprint after the run fixture
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
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 () => {
|
||||
|
|
@ -49,7 +55,11 @@ 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('Invalid format.')).toBeVisible();
|
||||
await expect(
|
||||
frame
|
||||
.getByText('Expected format: <ntp-server>. Example: time.redhat.com')
|
||||
.nth(0),
|
||||
).toBeVisible();
|
||||
await frame.getByPlaceholder('Add NTP servers').fill('0.cz.pool.ntp.org');
|
||||
await frame.getByRole('button', { name: 'Add NTP server' }).click();
|
||||
await expect(frame.getByText('0.cz.pool.ntp.org')).toBeVisible();
|
||||
|
|
@ -76,12 +86,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();
|
||||
|
|
@ -108,7 +118,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,7 +11,6 @@ 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();
|
||||
|
||||
|
|
@ -39,7 +38,7 @@ export const test = oldTest.extend<WithCleanup>({
|
|||
async () => {
|
||||
await Promise.all(Array.from(cleanupFns).map(([, fn]) => fn()));
|
||||
},
|
||||
{ box: true }
|
||||
{ box: true },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
8
playwright/fixtures/customizations.ts
Normal file
8
playwright/fixtures/customizations.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// 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);
|
||||
18
playwright/fixtures/popupHandler.ts
Normal file
18
playwright/fixtures/popupHandler.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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 },
|
||||
],
|
||||
});
|
||||
12
playwright/global.setup.ts
Normal file
12
playwright/global.setup.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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,4 +1,7 @@
|
|||
import { type Page, expect } from '@playwright/test';
|
||||
import { execSync } from 'child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export const togglePreview = async (page: Page) => {
|
||||
const toggleSwitch = page.locator('#preview-toggle');
|
||||
|
|
@ -28,12 +31,57 @@ export const closePopupsIfExist = async (page: Page) => {
|
|||
page
|
||||
.locator('iframe[name="intercom-modal-frame"]')
|
||||
.contentFrame()
|
||||
.getByRole('button', { name: 'Close' }),
|
||||
.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)
|
||||
];
|
||||
|
||||
for (const locator of locatorsToCheck) {
|
||||
await page.addLocatorHandler(locator, async () => {
|
||||
await locator.first().click(); // There can be multiple toast pop-ups
|
||||
await locator.first().click({ timeout: 10_000, noWaitAfter: true }); // 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,4 +1,6 @@
|
|||
import { type Page, expect } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
import { closePopupsIfExist, isHosted, togglePreview } from './helpers';
|
||||
import { ibFrame } from './navHelpers';
|
||||
|
|
@ -21,6 +23,38 @@ 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');
|
||||
|
||||
|
|
@ -31,46 +65,74 @@ const loginCockpit = async (page: Page, user: string, password: string) => {
|
|||
// image-builder lives inside an iframe
|
||||
const frame = ibFrame(page);
|
||||
|
||||
// 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();
|
||||
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();
|
||||
|
||||
// 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 or email' })
|
||||
.fill(user);
|
||||
await page.getByRole('textbox', { name: 'Red Hat login' }).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,13 +1,20 @@
|
|||
import type { FrameLocator, Page } from '@playwright/test';
|
||||
import { expect, FrameLocator, Page } from '@playwright/test';
|
||||
|
||||
import { isHosted } from './helpers';
|
||||
import { getHostArch, getHostDistroName, 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 blueprint' }).click();
|
||||
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('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 { isHosted } from './helpers';
|
||||
import { closePopupsIfExist, isHosted } from './helpers';
|
||||
import { ibFrame, navigateToLandingPage } from './navHelpers';
|
||||
|
||||
/**
|
||||
|
|
@ -10,13 +10,15 @@ 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);
|
||||
await page.getByTestId('blueprint-card').getByText(blueprintName).click();
|
||||
// the clickable blueprint cards are a bit awkward, so use the
|
||||
// button's id instead
|
||||
await page.locator(`button[id="${blueprintName}"]`).click();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -29,7 +31,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
|
||||
|
|
@ -70,6 +72,8 @@ 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 () => {
|
||||
|
|
@ -82,22 +86,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
|
||||
}
|
||||
await frame
|
||||
.getByTestId('blueprint-card')
|
||||
.getByText(blueprintName)
|
||||
.click();
|
||||
|
||||
// the clickable blueprint cards are a bit awkward, so use the
|
||||
// button's id instead
|
||||
await frame.locator(`button[id="${blueprintName}"]`).click();
|
||||
await frame.getByRole('button', { name: 'Menu toggle' }).click();
|
||||
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).click();
|
||||
await frame.getByRole('button', { name: 'Delete' }).click();
|
||||
},
|
||||
{ box: true }
|
||||
{ box: true },
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -125,7 +129,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();
|
||||
|
|
@ -134,7 +138,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,14 +1,20 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import TOML from '@ltd/j-toml';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { isHosted } from './helpers/helpers';
|
||||
import { login } from './helpers/login';
|
||||
import { ibFrame } from './helpers/navHelpers';
|
||||
import { closePopupsIfExist, isHosted } from './helpers/helpers';
|
||||
import { ensureAuthenticated } from './helpers/login';
|
||||
import { ibFrame, navigateToLandingPage } from './helpers/navHelpers';
|
||||
|
||||
test.describe.serial('test', () => {
|
||||
const blueprintName = uuidv4();
|
||||
test('create blueprint', async ({ page }) => {
|
||||
await login(page);
|
||||
await ensureAuthenticated(page);
|
||||
await closePopupsIfExist(page);
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
frame.getByRole('heading', { name: 'Images About image builder' });
|
||||
|
|
@ -16,14 +22,16 @@ test.describe.serial('test', () => {
|
|||
await frame.getByTestId('blueprints-create-button').click();
|
||||
|
||||
frame.getByRole('heading', { name: 'Image output' });
|
||||
await frame.getByTestId('checkbox-guest-image').click();
|
||||
await frame
|
||||
.getByRole('checkbox', { name: /Virtualization guest image/i })
|
||||
.click();
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
|
||||
if (isHosted()) {
|
||||
frame.getByRole('heading', {
|
||||
name: 'Register systems using this image',
|
||||
});
|
||||
await page.getByTestId('register-later-radio').click();
|
||||
await page.getByRole('radio', { name: /Register later/i }).click();
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +72,11 @@ test.describe.serial('test', () => {
|
|||
frame.getByRole('heading', { name: 'Systemd services' });
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
|
||||
if (isHosted()) {
|
||||
frame.getByRole('heading', { name: 'Ansible Automation Platform' });
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
}
|
||||
|
||||
if (isHosted()) {
|
||||
frame.getByRole('heading', { name: 'First boot configuration' });
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
|
|
@ -79,22 +92,32 @@ test.describe.serial('test', () => {
|
|||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
||||
|
||||
await expect(
|
||||
frame.locator('.pf-v6-c-card__title-text').getByText(blueprintName)
|
||||
frame.locator('.pf-v6-c-card__title-text').getByText(
|
||||
// if the name is too long, the blueprint card will have a truncated name.
|
||||
blueprintName.length > 24
|
||||
? blueprintName.slice(0, 24) + '...'
|
||||
: blueprintName,
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
await login(page);
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
const frame = await ibFrame(page);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'Search input' })
|
||||
.fill(blueprintName);
|
||||
await frame.getByText(blueprintName, { exact: true }).first().click();
|
||||
// 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: 'Edit blueprint' }).click();
|
||||
await frame.getByRole('button', { name: 'Additional packages' }).click();
|
||||
|
|
@ -108,6 +131,7 @@ 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();
|
||||
|
|
@ -115,17 +139,23 @@ 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 login(page);
|
||||
await ensureAuthenticated(page);
|
||||
await closePopupsIfExist(page);
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
const frame = await ibFrame(page);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'Search input' })
|
||||
.fill(blueprintName);
|
||||
await frame.getByText(blueprintName, { exact: true }).first().click();
|
||||
// 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-build-image-menu-option').click();
|
||||
|
||||
// make sure the image is present
|
||||
|
|
@ -137,14 +167,148 @@ test.describe.serial('test', () => {
|
|||
});
|
||||
|
||||
test('delete blueprint', async ({ page }) => {
|
||||
await login(page);
|
||||
await ensureAuthenticated(page);
|
||||
await closePopupsIfExist(page);
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
const frame = await ibFrame(page);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'Search input' })
|
||||
.fill(blueprintName);
|
||||
await frame.getByText(blueprintName, { exact: true }).first().click();
|
||||
await frame.getByTestId('blueprint-action-menu-toggle').click();
|
||||
// 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.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
57
pr_check.sh
|
|
@ -1,57 +0,0 @@
|
|||
#!/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
|
||||
8
schutzbot/playwright.fmf
Normal file
8
schutzbot/playwright.fmf
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
sudo systemctl enable --now cockpit.socket
|
||||
|
||||
|
|
@ -19,10 +19,13 @@ sudo usermod -aG wheel admin
|
|||
echo "admin ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/admin-nopasswd"
|
||||
|
||||
function upload_artifacts {
|
||||
mkdir -p /tmp/artifacts/extra-screenshots
|
||||
USER="$(whoami)"
|
||||
sudo chown -R "$USER:$USER" playwright-report
|
||||
mv playwright-report /tmp/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
|
||||
}
|
||||
trap upload_artifacts EXIT
|
||||
|
||||
|
|
@ -73,10 +76,12 @@ 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 @@
|
|||
7b4735d287dd0950e0a6f47dde65b62b0f239da1
|
||||
cf0a810fd3b75fa27139746c4dfe72222e13dcba
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import NotificationsPortal from '@redhat-cloud-services/frontend-components-notifications/NotificationPortal';
|
||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import NotificationsProvider from '@redhat-cloud-services/frontend-components-notifications/NotificationsProvider';
|
||||
import '@patternfly/patternfly/patternfly-addons.css';
|
||||
|
||||
import { Router } from './Router';
|
||||
|
|
@ -26,8 +26,9 @@ const App = () => {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NotificationsPortal />
|
||||
<Router />
|
||||
<NotificationsProvider>
|
||||
<Router />
|
||||
</NotificationsProvider>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
78
src/AppCockpit.scss
Normal file
78
src/AppCockpit.scss
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
@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,11 +3,14 @@ import '@patternfly/patternfly/patternfly-addons.css';
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import NotificationsPortal from '@redhat-cloud-services/frontend-components-notifications/NotificationPortal';
|
||||
import 'cockpit-dark-theme';
|
||||
import { Page, PageSection } from '@patternfly/react-core';
|
||||
import NotificationsProvider from '@redhat-cloud-services/frontend-components-notifications/NotificationsProvider';
|
||||
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';
|
||||
|
|
@ -28,16 +31,21 @@ const Application = () => {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NotificationsPortal />
|
||||
<HashRouter>
|
||||
<Router />
|
||||
</HashRouter>
|
||||
<NotificationsProvider>
|
||||
<HashRouter>
|
||||
<Router />
|
||||
</HashRouter>
|
||||
</NotificationsProvider>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const ImageBuilder = () => (
|
||||
<Provider store={store}>
|
||||
<Application />
|
||||
<Page className='no-masthead-sidebar' isContentFilled>
|
||||
<PageSection>
|
||||
<Application />
|
||||
</PageSection>
|
||||
</Page>
|
||||
</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,11 +58,10 @@ export const BlueprintActionsMenu: React.FunctionComponent<
|
|||
ref={toggleRef}
|
||||
isExpanded={showBlueprintActionsMenu}
|
||||
onClick={() => setShowBlueprintActionsMenu(!showBlueprintActionsMenu)}
|
||||
variant="plain"
|
||||
aria-label="blueprint menu toggle"
|
||||
data-testid="blueprint-action-menu-toggle"
|
||||
variant='plain'
|
||||
aria-label='blueprint menu toggle'
|
||||
>
|
||||
<EllipsisVIcon aria-hidden="true" />
|
||||
<EllipsisVIcon aria-hidden='true' />
|
||||
</MenuToggle>
|
||||
)}
|
||||
>
|
||||
|
|
@ -82,7 +81,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,22 +3,20 @@ import React from 'react';
|
|||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
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,
|
||||
useDeleteBlueprintMutation,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import { BlueprintItem } from '../../store/imageBuilderApi';
|
||||
|
||||
type blueprintProps = {
|
||||
blueprint: BlueprintItem;
|
||||
|
|
@ -28,26 +26,45 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
|
|||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [, { isLoading }] = useDeleteBlueprintMutation({
|
||||
const { isLoading } = useDeleteBlueprintMutation({
|
||||
fixedCacheKey: 'delete-blueprint',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
isSelected={blueprint.id === selectedBlueprintId}
|
||||
isSelectable
|
||||
isClicked={blueprint.id === selectedBlueprintId}
|
||||
data-testid={`blueprint-card`}
|
||||
isCompact
|
||||
isClickable
|
||||
onClick={() => dispatch(setBlueprintId(blueprint.id))}
|
||||
>
|
||||
<CardHeader data-testid={blueprint.id}>
|
||||
<CardTitle>
|
||||
<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}>
|
||||
{isLoading && blueprint.id === selectedBlueprintId && (
|
||||
<Spinner size="md" />
|
||||
<Spinner size='md' />
|
||||
)}
|
||||
{blueprint.name}
|
||||
{
|
||||
// NOTE: This might be an issue with the pf6 truncate component.
|
||||
// Since we're not really using the popover, we can just
|
||||
// use vanilla js to truncate the string rather than use the
|
||||
// Truncate component. We can match the behaviour of the component
|
||||
// by also splitting on 24 characters.
|
||||
// https://github.com/patternfly/patternfly-react/issues/11964
|
||||
blueprint.name && blueprint.name.length > 24
|
||||
? blueprint.name.slice(0, 24) + '...'
|
||||
: blueprint.name
|
||||
}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>{blueprint.description}</CardBody>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
import { DiffEditor } from '@monaco-editor/react';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { Modal, ModalVariant } from '@patternfly/react-core/deprecated';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalVariant,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { BuildImagesButton } from './BuildImagesButton';
|
||||
|
||||
|
|
@ -28,11 +34,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) {
|
||||
|
|
@ -40,32 +46,32 @@ const BlueprintDiffModal = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.large}
|
||||
titleIconVariant={'info'}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={`Compare ${blueprintName || ''} versions`}
|
||||
actions={[
|
||||
<BuildImagesButton key="build-button">
|
||||
<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'>
|
||||
Synchronize images
|
||||
</BuildImagesButton>,
|
||||
</BuildImagesButton>
|
||||
<Button
|
||||
key="cancel-button"
|
||||
variant="link"
|
||||
type="button"
|
||||
key='cancel-button'
|
||||
variant='link'
|
||||
type='button'
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<DiffEditor
|
||||
height="90vh"
|
||||
language="json"
|
||||
original={JSON.stringify(baseBlueprint, undefined, 2)}
|
||||
modified={JSON.stringify(blueprint, undefined, 2)}
|
||||
/>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</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, useEffect, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
Bullseye,
|
||||
|
|
@ -17,7 +17,6 @@ import {
|
|||
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
||||
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
|
@ -26,9 +25,10 @@ import BlueprintsPagination from './BlueprintsPagination';
|
|||
|
||||
import {
|
||||
DEBOUNCED_SEARCH_WAIT_TIME,
|
||||
PAGINATION_OFFSET,
|
||||
PAGINATION_LIMIT,
|
||||
PAGINATION_OFFSET,
|
||||
} from '../../constants';
|
||||
import { useGetUser } from '../../Hooks';
|
||||
import { useGetBlueprintsQuery } from '../../store/backendApi';
|
||||
import {
|
||||
selectBlueprintSearchInput,
|
||||
|
|
@ -46,7 +46,6 @@ import {
|
|||
} from '../../store/imageBuilderApi';
|
||||
import { imageBuilderApi } from '../../store/service/enhancedImageBuilderApi';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useGetEnvironment } from '../../Utilities/useGetEnvironment';
|
||||
|
||||
type blueprintSearchProps = {
|
||||
blueprintsTotal: number;
|
||||
|
|
@ -61,9 +60,8 @@ 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);
|
||||
|
|
@ -75,13 +73,6 @@ const BlueprintsSidebar = () => {
|
|||
offset: blueprintsOffset,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await auth?.getUser();
|
||||
setUserData(data);
|
||||
})();
|
||||
}, [auth]);
|
||||
|
||||
if (blueprintSearchInput) {
|
||||
searchParams.search = blueprintSearchInput;
|
||||
}
|
||||
|
|
@ -99,7 +90,7 @@ const BlueprintsSidebar = () => {
|
|||
if (isLoading) {
|
||||
return (
|
||||
<Bullseye>
|
||||
<Spinner size="xl" />
|
||||
<Spinner size='xl' />
|
||||
</Bullseye>
|
||||
);
|
||||
}
|
||||
|
|
@ -113,8 +104,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.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -124,8 +115,8 @@ const BlueprintsSidebar = () => {
|
|||
dispatch(setBlueprintId(undefined));
|
||||
};
|
||||
|
||||
if (!process.env.IS_ON_PREMISE && !isFedoraEnv) {
|
||||
const orgId = userData?.identity?.internal?.org_id;
|
||||
if (!process.env.IS_ON_PREMISE) {
|
||||
const orgId = userData?.identity.internal?.org_id;
|
||||
|
||||
analytics.group(orgId, {
|
||||
imagebuilder_blueprint_count: blueprintsData?.meta.count,
|
||||
|
|
@ -146,7 +137,7 @@ const BlueprintsSidebar = () => {
|
|||
<Flex justifyContent={{ default: 'justifyContentCenter' }}>
|
||||
<FlexItem>
|
||||
<Button
|
||||
variant="link"
|
||||
variant='link'
|
||||
isDisabled={!selectedBlueprintId}
|
||||
onClick={handleClickViewAll}
|
||||
>
|
||||
|
|
@ -162,14 +153,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 &&
|
||||
|
|
@ -193,7 +184,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 () => {
|
||||
|
|
@ -211,7 +202,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`}
|
||||
|
|
@ -225,7 +216,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,32 +1,31 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Dropdown,
|
||||
MenuToggle,
|
||||
Menu,
|
||||
MenuContent,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Flex,
|
||||
FlexItem,
|
||||
Spinner,
|
||||
Menu,
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
MenuToggle,
|
||||
MenuToggleAction,
|
||||
ButtonProps,
|
||||
Button,
|
||||
Spinner,
|
||||
} 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 {
|
||||
useGetBlueprintQuery,
|
||||
useComposeBlueprintMutation,
|
||||
} from '../../store/backendApi';
|
||||
useComposeBPWithNotification as useComposeBlueprintMutation,
|
||||
useGetUser,
|
||||
} from '../../Hooks';
|
||||
import { useGetBlueprintQuery } from '../../store/backendApi';
|
||||
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||
import { useAppSelector } from '../../store/hooks';
|
||||
import { ImageTypes } from '../../store/imageBuilderApi';
|
||||
|
||||
type BuildImagesButtonPropTypes = {
|
||||
|
|
@ -37,44 +36,27 @@ type BuildImagesButtonPropTypes = {
|
|||
export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||
const [deselectedTargets, setDeselectedTargets] = useState<ImageTypes[]>([]);
|
||||
const [buildBlueprint, { isLoading: imageBuildLoading }] =
|
||||
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
||||
useComposeBlueprintMutation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { analytics, auth } = useChrome();
|
||||
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await auth?.getUser();
|
||||
setUserData(data);
|
||||
})();
|
||||
}, [auth]);
|
||||
const { userData } = useGetUser(auth);
|
||||
|
||||
const onBuildHandler = async () => {
|
||||
if (selectedBlueprintId) {
|
||||
try {
|
||||
await buildBlueprint({
|
||||
id: selectedBlueprintId,
|
||||
body: {
|
||||
image_types: blueprintImageType?.filter(
|
||||
(target) => !deselectedTargets.includes(target)
|
||||
),
|
||||
},
|
||||
});
|
||||
await buildBlueprint({
|
||||
id: selectedBlueprintId,
|
||||
body: {
|
||||
image_types: blueprintImageType?.filter(
|
||||
(target) => !deselectedTargets.includes(target),
|
||||
),
|
||||
},
|
||||
});
|
||||
if (!process.env.IS_ON_PREMISE) {
|
||||
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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -83,21 +65,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]);
|
||||
|
|
@ -110,17 +92,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
|
||||
|
|
@ -136,7 +118,7 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
|||
} as React.CSSProperties
|
||||
}
|
||||
isInline
|
||||
size="md"
|
||||
size='md'
|
||||
/>
|
||||
</FlexItem>
|
||||
)}
|
||||
|
|
@ -180,7 +162,7 @@ export const BuildImagesButtonEmptyState = ({
|
|||
children,
|
||||
}: BuildImagesButtonEmptyStatePropTypes) => {
|
||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||
const [buildBlueprint, { isLoading: imageBuildLoading }] =
|
||||
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
||||
useComposeBlueprintMutation();
|
||||
const onBuildHandler = async () => {
|
||||
if (selectedBlueprintId) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { ActionGroup, Button } from '@patternfly/react-core';
|
||||
import { Modal, ModalVariant } from '@patternfly/react-core/deprecated';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalVariant,
|
||||
} from '@patternfly/react-core';
|
||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||
|
||||
import {
|
||||
AMPLITUDE_MODULE_NAME,
|
||||
|
|
@ -11,10 +16,10 @@ import {
|
|||
PAGINATION_OFFSET,
|
||||
} from '../../constants';
|
||||
import {
|
||||
backendApi,
|
||||
useDeleteBlueprintMutation,
|
||||
useGetBlueprintsQuery,
|
||||
} from '../../store/backendApi';
|
||||
useDeleteBPWithNotification as useDeleteBlueprintMutation,
|
||||
useGetUser,
|
||||
} from '../../Hooks';
|
||||
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
||||
import {
|
||||
selectBlueprintSearchInput,
|
||||
selectLimit,
|
||||
|
|
@ -39,14 +44,7 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
|||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||
const dispatch = useAppDispatch();
|
||||
const { analytics, auth } = useChrome();
|
||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await auth?.getUser();
|
||||
setUserData(data);
|
||||
})();
|
||||
}, [auth]);
|
||||
const { userData } = useGetUser(auth);
|
||||
|
||||
const searchParams: GetBlueprintsApiArg = {
|
||||
limit: blueprintsLimit,
|
||||
|
|
@ -61,19 +59,21 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
|||
selectFromResult: ({ data }) => ({
|
||||
blueprintName: data?.data.find(
|
||||
(blueprint: { id: string | undefined }) =>
|
||||
blueprint.id === selectedBlueprintId
|
||||
blueprint.id === selectedBlueprintId,
|
||||
)?.name,
|
||||
}),
|
||||
});
|
||||
const [deleteBlueprint] = useDeleteBlueprintMutation({
|
||||
const { trigger: deleteBlueprint } = useDeleteBlueprintMutation({
|
||||
fixedCacheKey: 'delete-blueprint',
|
||||
});
|
||||
const handleDelete = async () => {
|
||||
if (selectedBlueprintId) {
|
||||
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Deleted`, {
|
||||
module: AMPLITUDE_MODULE_NAME,
|
||||
account_id: userData?.identity.internal?.account_id || 'Not found',
|
||||
});
|
||||
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',
|
||||
});
|
||||
}
|
||||
setShowDeleteModal(false);
|
||||
await deleteBlueprint({ id: selectedBlueprintId });
|
||||
dispatch(setBlueprintId(undefined));
|
||||
|
|
@ -84,22 +84,20 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
|||
setShowDeleteModal(false);
|
||||
};
|
||||
return (
|
||||
<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}>
|
||||
<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}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="link" type="button" onClick={onDeleteClose}>
|
||||
<Button variant='link' type='button' onClick={onDeleteClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const EditBlueprintButton = () => {
|
|||
onClick={() =>
|
||||
navigate(resolveRelPath(`imagewizard/${selectedBlueprintId}`))
|
||||
}
|
||||
variant="secondary"
|
||||
variant='secondary'
|
||||
>
|
||||
Edit blueprint
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
|
||||
import { parse } from '@ltd/j-toml';
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
Checkbox,
|
||||
FileUpload,
|
||||
|
|
@ -11,12 +10,16 @@ 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 { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||
import { useAddNotification } from '@redhat-cloud-services/frontend-components-notifications/hooks';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { mapOnPremToHosted } from './helpers/onPremToHostedBlueprintMapper';
|
||||
|
|
@ -26,7 +29,6 @@ import {
|
|||
ApiRepositoryRequest,
|
||||
useBulkImportRepositoriesMutation,
|
||||
} from '../../store/contentSourcesApi';
|
||||
import { useAppDispatch } from '../../store/hooks';
|
||||
import {
|
||||
BlueprintExportResponse,
|
||||
BlueprintItem,
|
||||
|
|
@ -64,24 +66,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 dispatch = useAppDispatch();
|
||||
const addNotification = useAddNotification();
|
||||
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 {
|
||||
|
|
@ -96,40 +98,34 @@ 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;
|
||||
}
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'Failed to import custom repositories',
|
||||
description: JSON.stringify(repository.warnings),
|
||||
})
|
||||
);
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'Failed to import custom repositories',
|
||||
description: JSON.stringify(repository.warnings),
|
||||
});
|
||||
});
|
||||
|
||||
if (importedRepositoryNames.length !== 0) {
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'info',
|
||||
title: 'Successfully imported custom repositories',
|
||||
description: importedRepositoryNames.join(', '),
|
||||
})
|
||||
);
|
||||
addNotification({
|
||||
variant: 'info',
|
||||
title: 'Successfully imported custom repositories',
|
||||
description: importedRepositoryNames.join(', '),
|
||||
});
|
||||
}
|
||||
return newCustomRepos;
|
||||
}
|
||||
} catch {
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'danger',
|
||||
title: 'Custom repositories import failed',
|
||||
})
|
||||
);
|
||||
addNotification({
|
||||
variant: 'danger',
|
||||
title: 'Custom repositories import failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,11 +139,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);
|
||||
|
|
@ -159,9 +155,8 @@ 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 ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +174,7 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
undefined;
|
||||
const importBlueprintState = mapExportRequestToState(
|
||||
blueprintExportedResponse,
|
||||
blueprintFromFile.image_requests || []
|
||||
blueprintFromFile.image_requests || [],
|
||||
);
|
||||
|
||||
setIsOnPrem(false);
|
||||
|
|
@ -189,7 +184,7 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
mapOnPremToHosted(blueprintFromFile);
|
||||
const importBlueprintState = mapExportRequestToState(
|
||||
blueprintFromFileMapped,
|
||||
[]
|
||||
[],
|
||||
);
|
||||
setIsOnPrem(true);
|
||||
setImportedBlueprint(importBlueprintState);
|
||||
|
|
@ -197,13 +192,11 @@ export const ImportBlueprintModal: React.FunctionComponent<
|
|||
}
|
||||
} catch (error) {
|
||||
setIsInvalidFormat(true);
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'File is not a valid blueprint',
|
||||
description: error?.data?.error?.message,
|
||||
})
|
||||
);
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'File is not a valid blueprint',
|
||||
description: error?.data?.error?.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
parseAndImport();
|
||||
|
|
@ -248,94 +241,98 @@ 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}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</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;
|
||||
}
|
||||
|
||||
|
|
|
|||
210
src/Components/CloudProviderConfig/AWSConfig.tsx
Normal file
210
src/Components/CloudProviderConfig/AWSConfig.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
146
src/Components/CloudProviderConfig/CloudProviderConfig.tsx
Normal file
146
src/Components/CloudProviderConfig/CloudProviderConfig.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
37
src/Components/CloudProviderConfig/validators/index.tsx
Normal file
37
src/Components/CloudProviderConfig/validators/index.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
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,27 +17,6 @@
|
|||
--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;
|
||||
|
|
@ -74,3 +53,8 @@ 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,22 +2,23 @@ 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/FileSystemTable';
|
||||
import { FileSystemContext } from './steps/FileSystem/components/FileSystemTable';
|
||||
import FirewallStep from './steps/Firewall';
|
||||
import FirstBootStep from './steps/FirstBoot';
|
||||
import HostnameStep from './steps/Hostname';
|
||||
|
|
@ -38,60 +39,59 @@ 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 {
|
||||
useFilesystemValidation,
|
||||
useSnapshotValidation,
|
||||
useFirstBootValidation,
|
||||
useAAPValidation,
|
||||
useDetailsValidation,
|
||||
useRegistrationValidation,
|
||||
useFilesystemValidation,
|
||||
useFirewallValidation,
|
||||
useFirstBootValidation,
|
||||
useHostnameValidation,
|
||||
useKernelValidation,
|
||||
useUsersValidation,
|
||||
useTimezoneValidation,
|
||||
useFirewallValidation,
|
||||
useServicesValidation,
|
||||
useLocaleValidation,
|
||||
useRegistrationValidation,
|
||||
useServicesValidation,
|
||||
useSnapshotValidation,
|
||||
useTimezoneValidation,
|
||||
useUsersValidation,
|
||||
} from './utilities/useValidation';
|
||||
import {
|
||||
isAwsAccountIdValid,
|
||||
isAzureTenantGUIDValid,
|
||||
isAzureSubscriptionIdValid,
|
||||
isAzureResourceGroupValid,
|
||||
isAzureSubscriptionIdValid,
|
||||
isAzureTenantGUIDValid,
|
||||
isGcpEmailValid,
|
||||
} from './validators';
|
||||
|
||||
import {
|
||||
RHEL_8,
|
||||
RHEL_10_BETA,
|
||||
RHEL_10,
|
||||
AARCH64,
|
||||
CENTOS_9,
|
||||
AMPLITUDE_MODULE_NAME,
|
||||
RHEL_10,
|
||||
RHEL_8,
|
||||
RHEL_9,
|
||||
} from '../../constants';
|
||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||
import './CreateImageWizard.scss';
|
||||
import {
|
||||
changeDistribution,
|
||||
addImageType,
|
||||
changeArchitecture,
|
||||
changeAwsShareMethod,
|
||||
changeDistribution,
|
||||
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, useGetEnvironment } from '../../Utilities/useGetEnvironment';
|
||||
import { useFlag } 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,23 +196,19 @@ 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 isUsersEnabled = useFlag('image-builder.users.enabled');
|
||||
const isAAPRegistrationEnabled = useFlag('image-builder.aap.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') === 'rhel10beta') {
|
||||
dispatch(changeDistribution(RHEL_10_BETA));
|
||||
if (searchParams.get('release') === 'rhel9') {
|
||||
dispatch(changeDistribution(RHEL_9));
|
||||
}
|
||||
if (searchParams.get('release') === 'rhel10') {
|
||||
dispatch(changeDistribution(RHEL_10));
|
||||
|
|
@ -237,6 +233,10 @@ 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,11 +264,9 @@ 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
|
||||
|
|
@ -288,6 +286,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
const firewallValidation = useFirewallValidation();
|
||||
// Services
|
||||
const servicesValidation = useServicesValidation();
|
||||
// AAP
|
||||
const aapValidation = useAAPValidation();
|
||||
// Firstboot
|
||||
const firstBootValidation = useFirstBootValidation();
|
||||
// Details
|
||||
|
|
@ -295,45 +295,51 @@ 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 = 22;
|
||||
startIndex = JUMP_TO_REVIEW_STEP;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
if (process.env.IS_ON_PREMISE) {
|
||||
if (step.id === 'step-oscap' && step.isVisited) {
|
||||
useEffect(() => {
|
||||
if (process.env.IS_ON_PREMISE) {
|
||||
if (step.id === 'step-oscap' && step.isVisited) {
|
||||
setWasRegisterVisited(true);
|
||||
}
|
||||
} else if (step.id === 'step-register' && step.isVisited) {
|
||||
setWasRegisterVisited(true);
|
||||
}
|
||||
} else if (step.id === 'step-register' && step.isVisited) {
|
||||
setWasRegisterVisited(true);
|
||||
}
|
||||
}, [step.id, step.isVisited]);
|
||||
|
||||
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 &&
|
||||
|
|
@ -350,7 +356,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
{
|
||||
module: AMPLITUDE_MODULE_NAME,
|
||||
isPreview: isBeta(),
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
|
@ -369,8 +375,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}
|
||||
|
|
@ -381,25 +387,29 @@ 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) =>
|
||||
target === 'aws' || target === 'gcp' || target === 'azure'
|
||||
(target: string) =>
|
||||
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={
|
||||
awsShareMethod === 'manual'
|
||||
? !isAwsAccountIdValid(awsAccountId)
|
||||
: awsSourceId === undefined
|
||||
// we don't need the account id for
|
||||
// on-prem aws.
|
||||
process.env.IS_ON_PREMISE
|
||||
? false
|
||||
: awsShareMethod === 'manual'
|
||||
? !isAwsAccountIdValid(awsAccountId)
|
||||
: awsSourceId === undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
|
@ -408,9 +418,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={
|
||||
|
|
@ -424,21 +434,15 @@ 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={
|
||||
azureShareMethod === 'manual'
|
||||
? !isAzureTenantGUIDValid(azureTenantId) ||
|
||||
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
|
||||
!isAzureResourceGroupValid(azureResourceGroup)
|
||||
: azureShareMethod === 'sources'
|
||||
? !isAzureTenantGUIDValid(azureTenantId) ||
|
||||
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
|
||||
!isAzureResourceGroupValid(azureResourceGroup)
|
||||
: azureSource === undefined
|
||||
!isAzureTenantGUIDValid(azureTenantId) ||
|
||||
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
|
||||
!isAzureResourceGroupValid(azureResourceGroup)
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
|
@ -449,15 +453,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
|
||||
|
|
@ -476,10 +480,9 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
</WizardStep>,
|
||||
<WizardStep
|
||||
name={complianceEnabled ? 'Compliance' : 'OpenSCAP'}
|
||||
id="step-oscap"
|
||||
key="step-oscap"
|
||||
isHidden={distribution === RHEL_10_BETA}
|
||||
navItem={customStatusNavItem}
|
||||
id='step-oscap'
|
||||
key='step-oscap'
|
||||
navItem={CustomStatusNavItem}
|
||||
footer={
|
||||
<CustomWizardFooter disableNext={false} optional={true} />
|
||||
}
|
||||
|
|
@ -487,10 +490,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<OscapStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name="File system configuration"
|
||||
id="step-file-system"
|
||||
key="step-file-system"
|
||||
navItem={customStatusNavItem}
|
||||
name='File system configuration'
|
||||
id='step-file-system'
|
||||
key='step-file-system'
|
||||
navItem={CustomStatusNavItem}
|
||||
isHidden={hasWslTargetOnly}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
beforeNext={() => {
|
||||
|
|
@ -512,16 +516,12 @@ 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={
|
||||
distribution === RHEL_10_BETA ||
|
||||
!!process.env.IS_ON_PREMISE ||
|
||||
isFedoraEnv
|
||||
}
|
||||
isHidden={!!process.env.IS_ON_PREMISE}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={snapshotValidation.disabledNext}
|
||||
|
|
@ -532,15 +532,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<SnapshotStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name="Custom repositories"
|
||||
id="wizard-custom-repositories"
|
||||
key="wizard-custom-repositories"
|
||||
navItem={customStatusNavItem}
|
||||
isHidden={
|
||||
distribution === RHEL_10_BETA ||
|
||||
!!process.env.IS_ON_PREMISE ||
|
||||
isFedoraEnv
|
||||
}
|
||||
name='Custom repositories'
|
||||
id='wizard-custom-repositories'
|
||||
key='wizard-custom-repositories'
|
||||
navItem={CustomStatusNavItem}
|
||||
isHidden={!!process.env.IS_ON_PREMISE}
|
||||
isDisabled={snapshotValidation.disabledNext}
|
||||
footer={
|
||||
<CustomWizardFooter disableNext={false} optional={true} />
|
||||
|
|
@ -549,11 +545,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<RepositoriesStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name="Additional packages"
|
||||
id="wizard-additional-packages"
|
||||
key="wizard-additional-packages"
|
||||
navItem={customStatusNavItem}
|
||||
isHidden={isFedoraEnv}
|
||||
name='Additional packages'
|
||||
id='wizard-additional-packages'
|
||||
key='wizard-additional-packages'
|
||||
navItem={CustomStatusNavItem}
|
||||
isDisabled={snapshotValidation.disabledNext}
|
||||
footer={
|
||||
<CustomWizardFooter disableNext={false} optional={true} />
|
||||
|
|
@ -562,11 +557,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<PackagesStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name="Users"
|
||||
id="wizard-users"
|
||||
key="wizard-users"
|
||||
navItem={customStatusNavItem}
|
||||
isHidden={!isUsersEnabled}
|
||||
name='Users'
|
||||
id='wizard-users'
|
||||
key='wizard-users'
|
||||
navItem={CustomStatusNavItem}
|
||||
status={usersValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -578,10 +572,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
|
||||
|
|
@ -593,10 +587,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
|
||||
|
|
@ -608,10 +602,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
|
||||
|
|
@ -623,10 +617,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<HostnameStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name="Kernel"
|
||||
id="wizard-kernel"
|
||||
key="wizard-kernel"
|
||||
navItem={customStatusNavItem}
|
||||
name='Kernel'
|
||||
id='wizard-kernel'
|
||||
key='wizard-kernel'
|
||||
navItem={CustomStatusNavItem}
|
||||
isHidden={hasWslTargetOnly}
|
||||
status={kernelValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
|
|
@ -638,10 +633,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
|
||||
|
|
@ -653,10 +648,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
|
||||
|
|
@ -668,12 +663,28 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
<ServicesStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name="First boot script configuration"
|
||||
id="wizard-first-boot"
|
||||
key="wizard-first-boot"
|
||||
navItem={customStatusNavItem}
|
||||
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}
|
||||
status={firstBootValidation.disabledNext ? 'error' : 'default'}
|
||||
isHidden={!!process.env.IS_ON_PREMISE || isFedoraEnv}
|
||||
isHidden={!!process.env.IS_ON_PREMISE}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={firstBootValidation.disabledNext}
|
||||
|
|
@ -686,9 +697,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
|
||||
|
|
@ -699,8 +710,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 { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||
import { useAddNotification } from '@redhat-cloud-services/frontend-components-notifications/hooks';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import CreateImageWizard from './CreateImageWizard';
|
||||
|
|
@ -13,19 +13,18 @@ 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(''));
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'No blueprint was imported',
|
||||
})
|
||||
);
|
||||
addNotification({
|
||||
variant: 'warning',
|
||||
title: 'No blueprint was imported',
|
||||
});
|
||||
}
|
||||
}, [blueprint, dispatch]);
|
||||
return <CreateImageWizard />;
|
||||
|
|
|
|||
|
|
@ -48,30 +48,73 @@ const LabelInput = ({
|
|||
const dispatch = useAppDispatch();
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [errorText, setErrorText] = useState(stepValidation.errors[fieldName]);
|
||||
const [onStepInputErrorText, setOnStepInputErrorText] = useState('');
|
||||
let [invalidImports, duplicateImports] = ['', ''];
|
||||
|
||||
if (stepValidation.errors[fieldName]) {
|
||||
[invalidImports, duplicateImports] =
|
||||
stepValidation.errors[fieldName].split('|');
|
||||
}
|
||||
|
||||
const onTextInputChange = (
|
||||
_event: React.FormEvent<HTMLInputElement>,
|
||||
value: string
|
||||
value: string,
|
||||
) => {
|
||||
setInputValue(value);
|
||||
setErrorText('');
|
||||
setOnStepInputErrorText('');
|
||||
};
|
||||
|
||||
const addItem = (value: string) => {
|
||||
if (list?.includes(value) || requiredList?.includes(value)) {
|
||||
setErrorText(`${item} already exists.`);
|
||||
setOnStepInputErrorText(`${item} already exists.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator(value)) {
|
||||
setErrorText('Invalid format.');
|
||||
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.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(addAction(value));
|
||||
setInputValue('');
|
||||
setErrorText('');
|
||||
setOnStepInputErrorText('');
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent, value: string) => {
|
||||
|
|
@ -87,14 +130,18 @@ const LabelInput = ({
|
|||
|
||||
const handleRemoveItem = (e: React.MouseEvent, value: string) => {
|
||||
dispatch(removeAction(value));
|
||||
setErrorText('');
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setInputValue('');
|
||||
setErrorText('');
|
||||
setOnStepInputErrorText('');
|
||||
};
|
||||
|
||||
const errors = [];
|
||||
if (onStepInputErrorText) errors.push(onStepInputErrorText);
|
||||
if (invalidImports) errors.push(invalidImports);
|
||||
if (duplicateImports) errors.push(duplicateImports);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInputGroup>
|
||||
|
|
@ -107,34 +154,38 @@ 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>
|
||||
{errorText && (
|
||||
{errors.length > 0 && (
|
||||
<HelperText>
|
||||
<HelperTextItem variant={'error'}>{errorText}</HelperTextItem>
|
||||
{errors.map((error, index) => (
|
||||
<HelperTextItem key={index} variant={'error'}>
|
||||
{error}
|
||||
</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>
|
||||
|
|
@ -143,7 +194,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,6 +138,7 @@ export const ValidatedInput = ({
|
|||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
...props
|
||||
}: ValidatedTextInputPropTypes) => {
|
||||
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
||||
|
||||
|
|
@ -158,16 +159,17 @@ 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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
DropEvent,
|
||||
FileUpload,
|
||||
FormGroup,
|
||||
FormHelperText,
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
||||
import {
|
||||
changeAapCallbackUrl,
|
||||
changeAapHostConfigKey,
|
||||
changeAapTlsCertificateAuthority,
|
||||
changeAapTlsConfirmation,
|
||||
selectAapCallbackUrl,
|
||||
selectAapHostConfigKey,
|
||||
selectAapTlsCertificateAuthority,
|
||||
selectAapTlsConfirmation,
|
||||
} from '../../../../../store/wizardSlice';
|
||||
import { useAAPValidation } from '../../../utilities/useValidation';
|
||||
import { ValidatedInputAndTextArea } from '../../../ValidatedInput';
|
||||
import { validateMultipleCertificates } from '../../../validators';
|
||||
|
||||
const AAPRegistration = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const callbackUrl = useAppSelector(selectAapCallbackUrl);
|
||||
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
|
||||
const tlsCertificateAuthority = useAppSelector(
|
||||
selectAapTlsCertificateAuthority,
|
||||
);
|
||||
const tlsConfirmation = useAppSelector(selectAapTlsConfirmation);
|
||||
const [isRejected, setIsRejected] = React.useState(false);
|
||||
const stepValidation = useAAPValidation();
|
||||
|
||||
const isHttpsUrl = callbackUrl?.toLowerCase().startsWith('https://') || false;
|
||||
const shouldShowCaInput = !isHttpsUrl || (isHttpsUrl && !tlsConfirmation);
|
||||
|
||||
const validated = stepValidation.errors['certificate']
|
||||
? 'error'
|
||||
: stepValidation.errors['certificate'] === undefined &&
|
||||
tlsCertificateAuthority &&
|
||||
validateMultipleCertificates(tlsCertificateAuthority).validCertificates
|
||||
.length > 0
|
||||
? 'success'
|
||||
: 'default';
|
||||
|
||||
const handleCallbackUrlChange = (value: string) => {
|
||||
dispatch(changeAapCallbackUrl(value));
|
||||
};
|
||||
|
||||
const handleHostConfigKeyChange = (value: string) => {
|
||||
dispatch(changeAapHostConfigKey(value));
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
dispatch(changeAapTlsCertificateAuthority(''));
|
||||
};
|
||||
|
||||
const handleTextChange = (
|
||||
_event: React.ChangeEvent<HTMLTextAreaElement>,
|
||||
value: string,
|
||||
) => {
|
||||
dispatch(changeAapTlsCertificateAuthority(value));
|
||||
setIsRejected(false);
|
||||
};
|
||||
|
||||
const handleDataChange = (_: DropEvent, value: string) => {
|
||||
dispatch(changeAapTlsCertificateAuthority(value));
|
||||
setIsRejected(false);
|
||||
};
|
||||
|
||||
const handleFileRejected = () => {
|
||||
dispatch(changeAapTlsCertificateAuthority(''));
|
||||
setIsRejected(true);
|
||||
};
|
||||
|
||||
const handleTlsConfirmationChange = (checked: boolean) => {
|
||||
dispatch(changeAapTlsConfirmation(checked));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup label='Ansible Callback URL' isRequired>
|
||||
<ValidatedInputAndTextArea
|
||||
value={callbackUrl || ''}
|
||||
onChange={(_event, value) => handleCallbackUrlChange(value.trim())}
|
||||
ariaLabel='ansible callback url'
|
||||
isRequired
|
||||
stepValidation={stepValidation}
|
||||
fieldName='callbackUrl'
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label='Host Config Key' isRequired>
|
||||
<ValidatedInputAndTextArea
|
||||
value={hostConfigKey || ''}
|
||||
onChange={(_event, value) => handleHostConfigKeyChange(value.trim())}
|
||||
ariaLabel='host config key'
|
||||
isRequired
|
||||
stepValidation={stepValidation}
|
||||
fieldName='hostConfigKey'
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{shouldShowCaInput && (
|
||||
<FormGroup label='Certificate authority (CA) for Ansible Controller'>
|
||||
<FileUpload
|
||||
id='aap-certificate-upload'
|
||||
type='text'
|
||||
value={tlsCertificateAuthority || ''}
|
||||
filename={tlsCertificateAuthority ? 'CA detected' : ''}
|
||||
onDataChange={handleDataChange}
|
||||
onTextChange={handleTextChange}
|
||||
onClearClick={handleClear}
|
||||
dropzoneProps={{
|
||||
accept: {
|
||||
'application/x-pem-file': ['.pem'],
|
||||
'application/x-x509-ca-cert': ['.cer', '.crt'],
|
||||
'application/pkix-cert': ['.der'],
|
||||
},
|
||||
maxSize: 512000,
|
||||
onDropRejected: handleFileRejected,
|
||||
}}
|
||||
validated={isRejected ? 'error' : validated}
|
||||
browseButtonText='Upload'
|
||||
allowEditingUploadedText={true}
|
||||
/>
|
||||
<FormHelperText>
|
||||
<HelperText>
|
||||
<HelperTextItem
|
||||
variant={
|
||||
isRejected || validated === 'error'
|
||||
? 'error'
|
||||
: validated === 'success'
|
||||
? 'success'
|
||||
: 'default'
|
||||
}
|
||||
>
|
||||
{isRejected
|
||||
? 'Must be a .PEM/.CER/.CRT file'
|
||||
: validated === 'error'
|
||||
? stepValidation.errors['certificate']
|
||||
: validated === 'success'
|
||||
? 'Certificate was uploaded'
|
||||
: 'Drag and drop a valid certificate file or upload one'}
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
)}
|
||||
{isHttpsUrl && (
|
||||
<FormGroup>
|
||||
<Checkbox
|
||||
id='tls-confirmation-checkbox'
|
||||
label='Insecure'
|
||||
isChecked={tlsConfirmation || false}
|
||||
onChange={(_event, checked) => handleTlsConfirmationChange(checked)}
|
||||
/>
|
||||
{stepValidation.errors['tlsConfirmation'] && (
|
||||
<FormHelperText>
|
||||
<HelperText>
|
||||
<HelperTextItem variant='error'>
|
||||
{stepValidation.errors['tlsConfirmation']}
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AAPRegistration;
|
||||
18
src/Components/CreateImageWizard/steps/AAP/index.tsx
Normal file
18
src/Components/CreateImageWizard/steps/AAP/index.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Form, Title } from '@patternfly/react-core';
|
||||
|
||||
import AAPRegistration from './components/AAPRegistration';
|
||||
|
||||
const AAPStep = () => {
|
||||
return (
|
||||
<Form>
|
||||
<Title headingLevel='h1' size='xl'>
|
||||
Ansible Automation Platform
|
||||
</Title>
|
||||
<AAPRegistration />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AAPStep;
|
||||
|
|
@ -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