Compare commits
16 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
080513ad6d | ||
|
|
7391652e17 | ||
|
|
9a17373234 | ||
|
|
d7f844b8b6 | ||
|
|
859b7cace8 | ||
|
|
3a83a14720 | ||
|
|
e61cb99f1b | ||
|
|
a5aa15cbcb | ||
|
|
44c3674072 | ||
|
|
4d783537fb | ||
|
|
0b96c64c93 | ||
|
|
0d917c3cd8 | ||
|
|
957700adcc | ||
|
|
fa0560ac4d | ||
|
|
0e7f5d9e7b | ||
|
|
e0dd33fdc9 |
37 changed files with 1291 additions and 1258 deletions
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
|
||||||
|
|
@ -32,8 +32,7 @@ test:
|
||||||
- RUNNER:
|
- RUNNER:
|
||||||
- aws/fedora-41-x86_64
|
- aws/fedora-41-x86_64
|
||||||
- aws/fedora-42-x86_64
|
- aws/fedora-42-x86_64
|
||||||
- aws/rhel-9.6-nightly-x86_64
|
- aws/rhel-10.1-nightly-x86_64
|
||||||
- aws/rhel-10.0-nightly-x86_64
|
|
||||||
INTERNAL_NETWORK: ["true"]
|
INTERNAL_NETWORK: ["true"]
|
||||||
|
|
||||||
finish:
|
finish:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Name: cockpit-image-builder
|
Name: cockpit-image-builder
|
||||||
Version: 74
|
Version: 76
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Image builder plugin for Cockpit
|
Summary: Image builder plugin for Cockpit
|
||||||
|
|
||||||
|
|
|
||||||
1039
package-lock.json
generated
1039
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -47,13 +47,13 @@
|
||||||
"@testing-library/jest-dom": "6.6.4",
|
"@testing-library/jest-dom": "6.6.4",
|
||||||
"@testing-library/react": "16.3.0",
|
"@testing-library/react": "16.3.0",
|
||||||
"@testing-library/user-event": "14.6.1",
|
"@testing-library/user-event": "14.6.1",
|
||||||
"@types/node": "24.1.0",
|
"@types/node": "24.3.0",
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "18.3.12",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "18.3.1",
|
||||||
"@types/react-redux": "7.1.34",
|
"@types/react-redux": "7.1.34",
|
||||||
"@types/uuid": "10.0.0",
|
"@types/uuid": "10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "8.39.1",
|
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||||
"@typescript-eslint/parser": "8.39.1",
|
"@typescript-eslint/parser": "8.40.0",
|
||||||
"@vitejs/plugin-react": "4.7.0",
|
"@vitejs/plugin-react": "4.7.0",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"babel-loader": "10.0.0",
|
"babel-loader": "10.0.0",
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
"madge": "8.0.0",
|
"madge": "8.0.0",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"msw": "2.10.4",
|
"msw": "2.10.5",
|
||||||
"npm-run-all": "4.1.5",
|
"npm-run-all": "4.1.5",
|
||||||
"path-browserify": "1.0.1",
|
"path-browserify": "1.0.1",
|
||||||
"postcss-scss": "4.0.9",
|
"postcss-scss": "4.0.9",
|
||||||
|
|
|
||||||
10
packit.yaml
10
packit.yaml
|
|
@ -16,6 +16,15 @@ srpm_build_deps:
|
||||||
- npm
|
- npm
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
- job: tests
|
||||||
|
identifier: self
|
||||||
|
trigger: pull_request
|
||||||
|
tmt_plan: /plans/all/main
|
||||||
|
targets:
|
||||||
|
- centos-stream-10
|
||||||
|
- fedora-41
|
||||||
|
- fedora-42
|
||||||
|
|
||||||
- job: copr_build
|
- job: copr_build
|
||||||
trigger: pull_request
|
trigger: pull_request
|
||||||
targets: &build_targets
|
targets: &build_targets
|
||||||
|
|
@ -24,7 +33,6 @@ jobs:
|
||||||
- centos-stream-10
|
- centos-stream-10
|
||||||
- centos-stream-10-aarch64
|
- centos-stream-10-aarch64
|
||||||
- fedora-all
|
- fedora-all
|
||||||
- fedora-all-aarch64
|
|
||||||
|
|
||||||
- job: copr_build
|
- job: copr_build
|
||||||
trigger: commit
|
trigger: commit
|
||||||
|
|
|
||||||
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
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
import { expect, type Page } from '@playwright/test';
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
export const togglePreview = async (page: Page) => {
|
export const togglePreview = async (page: Page) => {
|
||||||
|
|
@ -42,3 +45,43 @@ export const closePopupsIfExist = async (page: Page) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// copied over from constants
|
||||||
|
const ON_PREM_RELEASES = new Map([
|
||||||
|
['centos-10', 'CentOS Stream 10'],
|
||||||
|
['fedora-41', 'Fedora Linux 41'],
|
||||||
|
['fedora-42', 'Fedora Linux 42'],
|
||||||
|
['rhel-10', 'Red Hat Enterprise Linux (RHEL) 10'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
export const getHostDistroName = (): string => {
|
||||||
|
const osRelData = readFileSync('/etc/os-release');
|
||||||
|
const lines = osRelData
|
||||||
|
.toString('utf-8')
|
||||||
|
.split('\n')
|
||||||
|
.filter((l) => l !== '');
|
||||||
|
const osRel = {};
|
||||||
|
|
||||||
|
for (const l of lines) {
|
||||||
|
const lineData = l.split('=');
|
||||||
|
(osRel as any)[lineData[0]] = lineData[1].replace(/"/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip minor version from rhel
|
||||||
|
const distro = ON_PREM_RELEASES.get(
|
||||||
|
`${(osRel as any)['ID']}-${(osRel as any)['VERSION_ID'].split('.')[0]}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distro === undefined) {
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
console.error('getHostDistroName failed, os-release config:', osRel);
|
||||||
|
throw new Error('getHostDistroName failed, distro undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return distro;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHostArch = (): string => {
|
||||||
|
return execSync('uname -m').toString('utf-8').replace(/\s/g, '');
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
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
|
* Opens the wizard, fills out the "Image Output" step, and navigates to the optional steps
|
||||||
|
|
@ -8,6 +8,13 @@ import { isHosted } from './helpers';
|
||||||
*/
|
*/
|
||||||
export const navigateToOptionalSteps = async (page: Page | FrameLocator) => {
|
export const navigateToOptionalSteps = async (page: Page | FrameLocator) => {
|
||||||
await page.getByRole('button', { name: 'Create image blueprint' }).click();
|
await page.getByRole('button', { name: 'Create 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('checkbox', { name: 'Virtualization' }).click();
|
||||||
await page.getByRole('button', { name: 'Next' }).click();
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,12 @@ test.describe.serial('test', () => {
|
||||||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
frame.locator('.pf-v6-c-card__title-text').getByText(blueprintName),
|
frame.locator('.pf-v6-c-card__title-text').getByText(
|
||||||
|
// if the name is too long, the blueprint card will have a truncated name.
|
||||||
|
blueprintName.length > 24
|
||||||
|
? blueprintName.slice(0, 24) + '...'
|
||||||
|
: blueprintName,
|
||||||
|
),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
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
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# As playwright isn't supported on fedora/el, install dependencies
|
TMT_SOURCE_DIR=${TMT_SOURCE_DIR:-}
|
||||||
# beforehand.
|
if [ -n "$TMT_SOURCE_DIR" ]; then
|
||||||
sudo dnf install -y \
|
# Move to the directory with sources
|
||||||
alsa-lib \
|
cd "${TMT_SOURCE_DIR}/cockpit-image-builder"
|
||||||
libXrandr-devel \
|
npm ci
|
||||||
libXdamage-devel \
|
elif [ "${CI:-}" != "true" ]; then
|
||||||
libXcomposite-devel \
|
# packit drops us into the schutzbot directory
|
||||||
at-spi2-atk-devel \
|
cd ../
|
||||||
cups \
|
npm ci
|
||||||
atk
|
fi
|
||||||
|
|
||||||
sudo systemctl enable --now cockpit.socket
|
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"
|
echo "admin ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/admin-nopasswd"
|
||||||
|
|
||||||
function upload_artifacts {
|
function upload_artifacts {
|
||||||
mkdir -p /tmp/artifacts/extra-screenshots
|
if [ -n "${TMT_TEST_DATA:-}" ]; then
|
||||||
USER="$(whoami)"
|
mv playwright-report "$TMT_TEST_DATA"/playwright-report
|
||||||
sudo chown -R "$USER:$USER" playwright-report
|
else
|
||||||
mv playwright-report /tmp/artifacts/
|
USER="$(whoami)"
|
||||||
|
sudo chown -R "$USER:$USER" playwright-report
|
||||||
|
mv playwright-report /tmp/artifacts/
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
trap upload_artifacts EXIT
|
trap upload_artifacts EXIT
|
||||||
|
|
||||||
|
|
@ -73,11 +76,12 @@ sudo podman run \
|
||||||
-e "CI=true" \
|
-e "CI=true" \
|
||||||
-e "PLAYWRIGHT_USER=admin" \
|
-e "PLAYWRIGHT_USER=admin" \
|
||||||
-e "PLAYWRIGHT_PASSWORD=foobar" \
|
-e "PLAYWRIGHT_PASSWORD=foobar" \
|
||||||
-e "CURRENTS_PROJECT_ID=$CURRENTS_PROJECT_ID" \
|
-e "CURRENTS_PROJECT_ID=${CURRENTS_PROJECT_ID:-}" \
|
||||||
-e "CURRENTS_RECORD_KEY=$CURRENTS_RECORD_KEY" \
|
-e "CURRENTS_RECORD_KEY=${CURRENTS_RECORD_KEY:-}" \
|
||||||
--net=host \
|
--net=host \
|
||||||
-v "$PWD:/tests" \
|
-v "$PWD:/tests" \
|
||||||
-v '/etc:/etc' \
|
-v '/etc:/etc' \
|
||||||
|
-v '/etc/os-release:/etc/os-release' \
|
||||||
--privileged \
|
--privileged \
|
||||||
--rm \
|
--rm \
|
||||||
--init \
|
--init \
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
7b4735d287dd0950e0a6f47dde65b62b0f239da1
|
cf0a810fd3b75fa27139746c4dfe72222e13dcba
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
Spinner,
|
Spinner,
|
||||||
Truncate,
|
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
|
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
|
||||||
|
|
@ -51,11 +50,21 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
|
||||||
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardTitle>
|
<CardTitle aria-label={blueprint.name}>
|
||||||
{isLoading && blueprint.id === selectedBlueprintId && (
|
{isLoading && blueprint.id === selectedBlueprintId && (
|
||||||
<Spinner size='md' />
|
<Spinner size='md' />
|
||||||
)}
|
)}
|
||||||
<Truncate content={blueprint.name} position='end' />
|
{
|
||||||
|
// NOTE: This might be an issue with the pf6 truncate component.
|
||||||
|
// Since we're not really using the popover, we can just
|
||||||
|
// use vanilla js to truncate the string rather than use the
|
||||||
|
// Truncate component. We can match the behaviour of the component
|
||||||
|
// by also splitting on 24 characters.
|
||||||
|
// https://github.com/patternfly/patternfly-react/issues/11964
|
||||||
|
blueprint.name && blueprint.name.length > 24
|
||||||
|
? blueprint.name.slice(0, 24) + '...'
|
||||||
|
: blueprint.name
|
||||||
|
}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>{blueprint.description}</CardBody>
|
<CardBody>{blueprint.description}</CardBody>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Bullseye,
|
Bullseye,
|
||||||
|
|
@ -17,7 +17,6 @@ import {
|
||||||
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
||||||
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
@ -29,6 +28,7 @@ import {
|
||||||
PAGINATION_LIMIT,
|
PAGINATION_LIMIT,
|
||||||
PAGINATION_OFFSET,
|
PAGINATION_OFFSET,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import { useGetUser } from '../../Hooks';
|
||||||
import { useGetBlueprintsQuery } from '../../store/backendApi';
|
import { useGetBlueprintsQuery } from '../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
selectBlueprintSearchInput,
|
selectBlueprintSearchInput,
|
||||||
|
|
@ -60,8 +60,8 @@ type emptyBlueprintStateProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const BlueprintsSidebar = () => {
|
const BlueprintsSidebar = () => {
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
|
|
||||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||||
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
||||||
|
|
@ -73,16 +73,6 @@ const BlueprintsSidebar = () => {
|
||||||
offset: blueprintsOffset,
|
offset: blueprintsOffset,
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (blueprintSearchInput) {
|
if (blueprintSearchInput) {
|
||||||
searchParams.search = blueprintSearchInput;
|
searchParams.search = blueprintSearchInput;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -16,11 +16,13 @@ import {
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
|
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
|
|
||||||
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
|
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
|
||||||
import { useComposeBPWithNotification as useComposeBlueprintMutation } from '../../Hooks';
|
import {
|
||||||
|
useComposeBPWithNotification as useComposeBlueprintMutation,
|
||||||
|
useGetUser,
|
||||||
|
} from '../../Hooks';
|
||||||
import { useGetBlueprintQuery } from '../../store/backendApi';
|
import { useGetBlueprintQuery } from '../../store/backendApi';
|
||||||
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
|
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
|
||||||
import { useAppSelector } from '../../store/hooks';
|
import { useAppSelector } from '../../store/hooks';
|
||||||
|
|
@ -37,18 +39,7 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
|
||||||
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
|
||||||
useComposeBlueprintMutation();
|
useComposeBlueprintMutation();
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onBuildHandler = async () => {
|
const onBuildHandler = async () => {
|
||||||
if (selectedBlueprintId) {
|
if (selectedBlueprintId) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -9,14 +9,16 @@ import {
|
||||||
ModalVariant,
|
ModalVariant,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AMPLITUDE_MODULE_NAME,
|
AMPLITUDE_MODULE_NAME,
|
||||||
PAGINATION_LIMIT,
|
PAGINATION_LIMIT,
|
||||||
PAGINATION_OFFSET,
|
PAGINATION_OFFSET,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
|
import {
|
||||||
|
useDeleteBPWithNotification as useDeleteBlueprintMutation,
|
||||||
|
useGetUser,
|
||||||
|
} from '../../Hooks';
|
||||||
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
selectBlueprintSearchInput,
|
selectBlueprintSearchInput,
|
||||||
|
|
@ -42,17 +44,7 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
||||||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
const { userData } = useGetUser(auth);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const searchParams: GetBlueprintsApiArg = {
|
const searchParams: GetBlueprintsApiArg = {
|
||||||
limit: blueprintsLimit,
|
limit: blueprintsLimit,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import {
|
||||||
Thead,
|
Thead,
|
||||||
Tr,
|
Tr,
|
||||||
} from '@patternfly/react-table';
|
} from '@patternfly/react-table';
|
||||||
|
import { orderBy } from 'lodash';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import CustomHelperText from './components/CustomHelperText';
|
import CustomHelperText from './components/CustomHelperText';
|
||||||
|
|
@ -66,7 +67,6 @@ import {
|
||||||
} from '../../../../constants';
|
} from '../../../../constants';
|
||||||
import { useGetArchitecturesQuery } from '../../../../store/backendApi';
|
import { useGetArchitecturesQuery } from '../../../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
ApiPackageSourcesResponse,
|
|
||||||
ApiRepositoryResponseRead,
|
ApiRepositoryResponseRead,
|
||||||
ApiSearchRpmResponse,
|
ApiSearchRpmResponse,
|
||||||
useCreateRepositoryMutation,
|
useCreateRepositoryMutation,
|
||||||
|
|
@ -700,7 +700,7 @@ const Packages = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpackedData: IBPackageWithRepositoryInfo[] =
|
let unpackedData: IBPackageWithRepositoryInfo[] =
|
||||||
combinedPackageData.flatMap((item) => {
|
combinedPackageData.flatMap((item) => {
|
||||||
// Spread modules into separate rows by application stream
|
// Spread modules into separate rows by application stream
|
||||||
if (item.sources) {
|
if (item.sources) {
|
||||||
|
|
@ -724,13 +724,16 @@ const Packages = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// group by name, but sort by application stream in descending order
|
// group by name, but sort by application stream in descending order
|
||||||
unpackedData.sort((a, b) => {
|
unpackedData = orderBy(
|
||||||
if (a.name === b.name) {
|
unpackedData,
|
||||||
return (b.stream ?? '').localeCompare(a.stream ?? '');
|
[
|
||||||
} else {
|
'name',
|
||||||
return a.name.localeCompare(b.name);
|
(pkg) => pkg.stream || '',
|
||||||
}
|
(pkg) => pkg.repository || '',
|
||||||
});
|
(pkg) => pkg.module_name || '',
|
||||||
|
],
|
||||||
|
['asc', 'desc', 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
|
||||||
if (toggleSelected === 'toggle-available') {
|
if (toggleSelected === 'toggle-available') {
|
||||||
if (activeTabKey === Repos.INCLUDED) {
|
if (activeTabKey === Repos.INCLUDED) {
|
||||||
|
|
@ -866,8 +869,6 @@ const Packages = () => {
|
||||||
dispatch(addPackage(pkg));
|
dispatch(addPackage(pkg));
|
||||||
if (pkg.type === 'module') {
|
if (pkg.type === 'module') {
|
||||||
setActiveStream(pkg.stream || '');
|
setActiveStream(pkg.stream || '');
|
||||||
setActiveSortIndex(2);
|
|
||||||
setPage(1);
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addModule({
|
addModule({
|
||||||
name: pkg.module_name || '',
|
name: pkg.module_name || '',
|
||||||
|
|
@ -993,7 +994,18 @@ const Packages = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialExpandedPkgs: IBPackageWithRepositoryInfo[] = [];
|
const getPackageUniqueKey = (pkg: IBPackageWithRepositoryInfo): string => {
|
||||||
|
try {
|
||||||
|
if (!pkg || !pkg.name) {
|
||||||
|
return `invalid_${Date.now()}`;
|
||||||
|
}
|
||||||
|
return `${pkg.name}_${pkg.stream || 'none'}_${pkg.module_name || 'none'}_${pkg.repository || 'unknown'}`;
|
||||||
|
} catch {
|
||||||
|
return `error_${Date.now()}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialExpandedPkgs: string[] = [];
|
||||||
const [expandedPkgs, setExpandedPkgs] = useState(initialExpandedPkgs);
|
const [expandedPkgs, setExpandedPkgs] = useState(initialExpandedPkgs);
|
||||||
|
|
||||||
const setPkgExpanded = (
|
const setPkgExpanded = (
|
||||||
|
|
@ -1001,12 +1013,13 @@ const Packages = () => {
|
||||||
isExpanding: boolean,
|
isExpanding: boolean,
|
||||||
) =>
|
) =>
|
||||||
setExpandedPkgs((prevExpanded) => {
|
setExpandedPkgs((prevExpanded) => {
|
||||||
const otherExpandedPkgs = prevExpanded.filter((p) => p.name !== pkg.name);
|
const pkgKey = getPackageUniqueKey(pkg);
|
||||||
return isExpanding ? [...otherExpandedPkgs, pkg] : otherExpandedPkgs;
|
const otherExpandedPkgs = prevExpanded.filter((key) => key !== pkgKey);
|
||||||
|
return isExpanding ? [...otherExpandedPkgs, pkgKey] : otherExpandedPkgs;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
||||||
expandedPkgs.includes(pkg);
|
expandedPkgs.includes(getPackageUniqueKey(pkg));
|
||||||
|
|
||||||
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
||||||
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
||||||
|
|
@ -1030,51 +1043,37 @@ const Packages = () => {
|
||||||
'asc' | 'desc'
|
'asc' | 'desc'
|
||||||
>('asc');
|
>('asc');
|
||||||
|
|
||||||
const getSortableRowValues = (
|
const sortedPackages = useMemo(() => {
|
||||||
pkg: IBPackageWithRepositoryInfo,
|
if (!transformedPackages || !Array.isArray(transformedPackages)) {
|
||||||
): (string | number | ApiPackageSourcesResponse[] | undefined)[] => {
|
return [];
|
||||||
return [pkg.name, pkg.summary, pkg.stream, pkg.end_date, pkg.repository];
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let sortedPackages = transformedPackages;
|
return orderBy(
|
||||||
sortedPackages = transformedPackages.sort((a, b) => {
|
transformedPackages,
|
||||||
const aValue = getSortableRowValues(a)[activeSortIndex];
|
[
|
||||||
const bValue = getSortableRowValues(b)[activeSortIndex];
|
// Active stream packages first (if activeStream is set)
|
||||||
if (typeof aValue === 'number') {
|
(pkg) => (activeStream && pkg.stream === activeStream ? 0 : 1),
|
||||||
// Numeric sort
|
// Then by name
|
||||||
if (activeSortDirection === 'asc') {
|
'name',
|
||||||
return (aValue as number) - (bValue as number);
|
// Then by stream version (descending)
|
||||||
}
|
(pkg) => {
|
||||||
return (bValue as number) - (aValue as number);
|
if (!pkg.stream) return '';
|
||||||
}
|
const parts = pkg.stream
|
||||||
// String sort
|
.split('.')
|
||||||
// if active stream is set, sort it to the top
|
.map((part) => parseInt(part, 10) || 0);
|
||||||
if (aValue === activeStream) {
|
// Convert to string with zero-padding for proper sorting
|
||||||
return -1;
|
return parts.map((p) => p.toString().padStart(10, '0')).join('.');
|
||||||
}
|
},
|
||||||
if (bValue === activeStream) {
|
// Then by end date (nulls last)
|
||||||
return 1;
|
(pkg) => pkg.end_date || '9999-12-31',
|
||||||
}
|
// Then by repository
|
||||||
if (activeSortDirection === 'asc') {
|
(pkg) => pkg.repository || '',
|
||||||
// handle packages with undefined stream
|
// Finally by module name
|
||||||
if (!aValue) {
|
(pkg) => pkg.module_name || '',
|
||||||
return -1;
|
],
|
||||||
}
|
['asc', 'asc', 'desc', 'asc', 'asc', 'asc'],
|
||||||
if (!bValue) {
|
);
|
||||||
return 1;
|
}, [transformedPackages, activeStream]);
|
||||||
}
|
|
||||||
return (aValue as string).localeCompare(bValue as string);
|
|
||||||
} else {
|
|
||||||
// handle packages with undefined stream
|
|
||||||
if (!aValue) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (!bValue) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return (bValue as string).localeCompare(aValue as string);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const getSortParams = (columnIndex: number) => ({
|
const getSortParams = (columnIndex: number) => ({
|
||||||
sortBy: {
|
sortBy: {
|
||||||
|
|
@ -1100,14 +1099,14 @@ const Packages = () => {
|
||||||
(module) => module.name === pkg.name,
|
(module) => module.name === pkg.name,
|
||||||
);
|
);
|
||||||
isSelected =
|
isSelected =
|
||||||
packages.some((p) => p.name === pkg.name) && !isModuleWithSameName;
|
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
||||||
|
!isModuleWithSameName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pkg.type === 'module') {
|
if (pkg.type === 'module') {
|
||||||
// the package is selected if it's added to the packages state
|
// the package is selected if its module stream matches one in enabled_modules
|
||||||
// and its module stream matches one in enabled_modules
|
|
||||||
isSelected =
|
isSelected =
|
||||||
packages.some((p) => p.name === pkg.name) &&
|
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
||||||
modules.some(
|
modules.some(
|
||||||
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
|
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
|
||||||
);
|
);
|
||||||
|
|
@ -1208,7 +1207,7 @@ const Packages = () => {
|
||||||
.slice(computeStart(), computeEnd())
|
.slice(computeStart(), computeEnd())
|
||||||
.map((grp, rowIndex) => (
|
.map((grp, rowIndex) => (
|
||||||
<Tbody
|
<Tbody
|
||||||
key={`${grp.name}-${rowIndex}`}
|
key={`${grp.name}-${grp.repository || 'default'}`}
|
||||||
isExpanded={isGroupExpanded(grp.name)}
|
isExpanded={isGroupExpanded(grp.name)}
|
||||||
>
|
>
|
||||||
<Tr data-testid='package-row'>
|
<Tr data-testid='package-row'>
|
||||||
|
|
@ -1308,7 +1307,7 @@ const Packages = () => {
|
||||||
.slice(computeStart(), computeEnd())
|
.slice(computeStart(), computeEnd())
|
||||||
.map((pkg, rowIndex) => (
|
.map((pkg, rowIndex) => (
|
||||||
<Tbody
|
<Tbody
|
||||||
key={`${pkg.name}-${rowIndex}`}
|
key={`${pkg.name}-${pkg.stream || 'default'}-${pkg.module_name || pkg.name}`}
|
||||||
isExpanded={isPkgExpanded(pkg)}
|
isExpanded={isPkgExpanded(pkg)}
|
||||||
>
|
>
|
||||||
<Tr data-testid='package-row'>
|
<Tr data-testid='package-row'>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopy,
|
ClipboardCopy,
|
||||||
|
|
@ -16,6 +16,7 @@ import ActivationKeysList from './components/ActivationKeysList';
|
||||||
import Registration from './components/Registration';
|
import Registration from './components/Registration';
|
||||||
import SatelliteRegistration from './components/SatelliteRegistration';
|
import SatelliteRegistration from './components/SatelliteRegistration';
|
||||||
|
|
||||||
|
import { useGetUser } from '../../../../Hooks';
|
||||||
import { useAppSelector } from '../../../../store/hooks';
|
import { useAppSelector } from '../../../../store/hooks';
|
||||||
import {
|
import {
|
||||||
selectActivationKey,
|
selectActivationKey,
|
||||||
|
|
@ -24,18 +25,7 @@ import {
|
||||||
|
|
||||||
const RegistrationStep = () => {
|
const RegistrationStep = () => {
|
||||||
const { auth } = useChrome();
|
const { auth } = useChrome();
|
||||||
const [orgId, setOrgId] = useState<string | undefined>(undefined);
|
const { orgId } = useGetUser(auth);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const userData = await auth.getUser();
|
|
||||||
const id = userData?.identity?.internal?.org_id;
|
|
||||||
setOrgId(id);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const activationKey = useAppSelector(selectActivationKey);
|
const activationKey = useAppSelector(selectActivationKey);
|
||||||
const registrationType = useAppSelector(selectRegistrationType);
|
const registrationType = useAppSelector(selectRegistrationType);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -14,12 +14,12 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
|
||||||
|
|
||||||
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
|
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
|
||||||
import {
|
import {
|
||||||
useComposeBPWithNotification as useComposeBlueprintMutation,
|
useComposeBPWithNotification as useComposeBlueprintMutation,
|
||||||
useCreateBPWithNotification as useCreateBlueprintMutation,
|
useCreateBPWithNotification as useCreateBlueprintMutation,
|
||||||
|
useGetUser,
|
||||||
} from '../../../../../Hooks';
|
} from '../../../../../Hooks';
|
||||||
import { setBlueprintId } from '../../../../../store/BlueprintSlice';
|
import { setBlueprintId } from '../../../../../store/BlueprintSlice';
|
||||||
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
|
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
|
||||||
|
|
@ -44,19 +44,8 @@ export const CreateSaveAndBuildBtn = ({
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: CreateDropdownProps) => {
|
}: CreateDropdownProps) => {
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
|
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const packages = useAppSelector(selectPackages);
|
const packages = useAppSelector(selectPackages);
|
||||||
|
|
||||||
|
|
@ -113,17 +102,7 @@ export const CreateSaveButton = ({
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: CreateDropdownProps) => {
|
}: CreateDropdownProps) => {
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = useChrome();
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
const { userData } = useGetUser(auth);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const packages = useAppSelector(selectPackages);
|
const packages = useAppSelector(selectPackages);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
|
|
@ -9,11 +9,11 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
|
||||||
|
|
||||||
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
|
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
|
||||||
import {
|
import {
|
||||||
useComposeBPWithNotification as useComposeBlueprintMutation,
|
useComposeBPWithNotification as useComposeBlueprintMutation,
|
||||||
|
useGetUser,
|
||||||
useUpdateBPWithNotification as useUpdateBlueprintMutation,
|
useUpdateBPWithNotification as useUpdateBlueprintMutation,
|
||||||
} from '../../../../../Hooks';
|
} from '../../../../../Hooks';
|
||||||
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
|
import { CockpitCreateBlueprintRequest } from '../../../../../store/cockpit/types';
|
||||||
|
|
@ -37,19 +37,8 @@ export const EditSaveAndBuildBtn = ({
|
||||||
blueprintId,
|
blueprintId,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: EditDropdownProps) => {
|
}: EditDropdownProps) => {
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
|
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { trigger: buildBlueprint } = useComposeBlueprintMutation();
|
const { trigger: buildBlueprint } = useComposeBlueprintMutation();
|
||||||
const packages = useAppSelector(selectPackages);
|
const packages = useAppSelector(selectPackages);
|
||||||
|
|
@ -105,19 +94,8 @@ export const EditSaveButton = ({
|
||||||
blueprintId,
|
blueprintId,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: EditDropdownProps) => {
|
}: EditDropdownProps) => {
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
|
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const packages = useAppSelector(selectPackages);
|
const packages = useAppSelector(selectPackages);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { EditSaveAndBuildBtn, EditSaveButton } from './EditDropdown';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useCreateBPWithNotification as useCreateBlueprintMutation,
|
useCreateBPWithNotification as useCreateBlueprintMutation,
|
||||||
|
useGetUser,
|
||||||
useUpdateBPWithNotification as useUpdateBlueprintMutation,
|
useUpdateBPWithNotification as useUpdateBlueprintMutation,
|
||||||
} from '../../../../../Hooks';
|
} from '../../../../../Hooks';
|
||||||
import { resolveRelPath } from '../../../../../Utilities/path';
|
import { resolveRelPath } from '../../../../../Utilities/path';
|
||||||
|
|
@ -33,6 +34,7 @@ const ReviewWizardFooter = () => {
|
||||||
const { isSuccess: isUpdateSuccess, reset: resetUpdate } =
|
const { isSuccess: isUpdateSuccess, reset: resetUpdate } =
|
||||||
useUpdateBlueprintMutation({ fixedCacheKey: 'updateBlueprintKey' });
|
useUpdateBlueprintMutation({ fixedCacheKey: 'updateBlueprintKey' });
|
||||||
const { auth } = useChrome();
|
const { auth } = useChrome();
|
||||||
|
const { orgId } = useGetUser(auth);
|
||||||
const { composeId } = useParams();
|
const { composeId } = useParams();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
@ -52,14 +54,12 @@ const ReviewWizardFooter = () => {
|
||||||
|
|
||||||
const getBlueprintPayload = async () => {
|
const getBlueprintPayload = async () => {
|
||||||
if (!process.env.IS_ON_PREMISE) {
|
if (!process.env.IS_ON_PREMISE) {
|
||||||
const userData = await auth.getUser();
|
|
||||||
const orgId = userData?.identity?.internal?.org_id;
|
|
||||||
const requestBody = orgId && mapRequestFromState(store, orgId);
|
const requestBody = orgId && mapRequestFromState(store, orgId);
|
||||||
return requestBody;
|
return requestBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: This should be fine on-prem, we should
|
// NOTE: This is fine for on prem because we save the org id
|
||||||
// be able to ignore the `org-id`
|
// to state through a form field in the registration step
|
||||||
return mapRequestFromState(store, '');
|
return mapRequestFromState(store, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,8 +211,9 @@ function commonRequestToState(
|
||||||
snapshot_date = '';
|
snapshot_date = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we need to check for the region for on-prem
|
||||||
const awsUploadOptions = aws?.upload_request
|
const awsUploadOptions = aws?.upload_request
|
||||||
.options as AwsUploadRequestOptions;
|
.options as AwsUploadRequestOptions & { region?: string | undefined };
|
||||||
const gcpUploadOptions = gcp?.upload_request
|
const gcpUploadOptions = gcp?.upload_request
|
||||||
.options as GcpUploadRequestOptions;
|
.options as GcpUploadRequestOptions;
|
||||||
const azureUploadOptions = azure?.upload_request
|
const azureUploadOptions = azure?.upload_request
|
||||||
|
|
@ -315,6 +316,7 @@ function commonRequestToState(
|
||||||
: 'manual') as AwsShareMethod,
|
: 'manual') as AwsShareMethod,
|
||||||
source: { id: awsUploadOptions?.share_with_sources?.[0] },
|
source: { id: awsUploadOptions?.share_with_sources?.[0] },
|
||||||
sourceId: awsUploadOptions?.share_with_sources?.[0],
|
sourceId: awsUploadOptions?.share_with_sources?.[0],
|
||||||
|
region: awsUploadOptions?.region,
|
||||||
},
|
},
|
||||||
snapshotting: {
|
snapshotting: {
|
||||||
useLatest: !snapshot_date && !request.image_requests[0]?.content_template,
|
useLatest: !snapshot_date && !request.image_requests[0]?.content_template,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
|
@ -13,11 +13,11 @@ import {
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
|
||||||
|
|
||||||
import ClonesTable from './ClonesTable';
|
import ClonesTable from './ClonesTable';
|
||||||
|
|
||||||
import { AMPLITUDE_MODULE_NAME } from '../../constants';
|
import { AMPLITUDE_MODULE_NAME } from '../../constants';
|
||||||
|
import { useGetUser } from '../../Hooks';
|
||||||
import { useGetComposeStatusQuery } from '../../store/backendApi';
|
import { useGetComposeStatusQuery } from '../../store/backendApi';
|
||||||
import { extractProvisioningList } from '../../store/helpers';
|
import { extractProvisioningList } from '../../store/helpers';
|
||||||
import {
|
import {
|
||||||
|
|
@ -134,19 +134,9 @@ type AwsDetailsPropTypes = {
|
||||||
|
|
||||||
export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => {
|
export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => {
|
||||||
const options = compose.request.image_requests[0].upload_request.options;
|
const options = compose.request.image_requests[0].upload_request.options;
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!isAwsUploadRequestOptions(options)) {
|
if (!isAwsUploadRequestOptions(options)) {
|
||||||
throw TypeError(
|
throw TypeError(
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
Tr,
|
Tr,
|
||||||
} from '@patternfly/react-table';
|
} from '@patternfly/react-table';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
import { useFlag } from '@unleash/proxy-client-react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { NavigateFunction, useNavigate } from 'react-router-dom';
|
import { NavigateFunction, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
@ -58,6 +58,7 @@ import {
|
||||||
SEARCH_INPUT,
|
SEARCH_INPUT,
|
||||||
STATUS_POLLING_INTERVAL,
|
STATUS_POLLING_INTERVAL,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import { useGetUser } from '../../Hooks';
|
||||||
import {
|
import {
|
||||||
useGetBlueprintComposesQuery,
|
useGetBlueprintComposesQuery,
|
||||||
useGetBlueprintsQuery,
|
useGetBlueprintsQuery,
|
||||||
|
|
@ -87,11 +88,12 @@ import {
|
||||||
timestampToDisplayString,
|
timestampToDisplayString,
|
||||||
timestampToDisplayStringDetailed,
|
timestampToDisplayStringDetailed,
|
||||||
} from '../../Utilities/time';
|
} from '../../Utilities/time';
|
||||||
|
import { AzureLaunchModal } from '../Launch/AzureLaunchModal';
|
||||||
|
import { OciLaunchModal } from '../Launch/OciLaunchModal';
|
||||||
|
|
||||||
const ImagesTable = () => {
|
const ImagesTable = () => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(10);
|
const [perPage, setPerPage] = useState(10);
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
|
|
||||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||||
const blueprintSearchInput =
|
const blueprintSearchInput =
|
||||||
|
|
@ -104,16 +106,7 @@ const ImagesTable = () => {
|
||||||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||||
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const searchParamsGetBlueprints: GetBlueprintsApiArg = {
|
const searchParamsGetBlueprints: GetBlueprintsApiArg = {
|
||||||
limit: blueprintsLimit,
|
limit: blueprintsLimit,
|
||||||
|
|
@ -382,8 +375,14 @@ type AzureRowPropTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AzureRow = ({ compose, rowIndex }: AzureRowPropTypes) => {
|
const AzureRow = ({ compose, rowIndex }: AzureRowPropTypes) => {
|
||||||
|
const launchEofFlag = useFlag('image-builder.launcheof');
|
||||||
|
|
||||||
const details = <AzureDetails compose={compose} />;
|
const details = <AzureDetails compose={compose} />;
|
||||||
const instance = <CloudInstance compose={compose} />;
|
const instance = launchEofFlag ? (
|
||||||
|
<AzureLaunchModal compose={compose} />
|
||||||
|
) : (
|
||||||
|
<CloudInstance compose={compose} />
|
||||||
|
);
|
||||||
const status = <CloudStatus compose={compose} />;
|
const status = <CloudStatus compose={compose} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -403,13 +402,18 @@ type OciRowPropTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const OciRow = ({ compose, rowIndex }: OciRowPropTypes) => {
|
const OciRow = ({ compose, rowIndex }: OciRowPropTypes) => {
|
||||||
|
const launchEofFlag = useFlag('image-builder.launcheof');
|
||||||
const daysToExpiration = Math.floor(
|
const daysToExpiration = Math.floor(
|
||||||
computeHoursToExpiration(compose.created_at) / 24,
|
computeHoursToExpiration(compose.created_at) / 24,
|
||||||
);
|
);
|
||||||
const isExpired = daysToExpiration >= OCI_STORAGE_EXPIRATION_TIME_IN_DAYS;
|
const isExpired = daysToExpiration >= OCI_STORAGE_EXPIRATION_TIME_IN_DAYS;
|
||||||
|
|
||||||
const details = <OciDetails compose={compose} />;
|
const details = <OciDetails compose={compose} />;
|
||||||
const instance = <OciInstance compose={compose} isExpired={isExpired} />;
|
const instance = launchEofFlag ? (
|
||||||
|
<OciLaunchModal compose={compose} isExpired={isExpired} />
|
||||||
|
) : (
|
||||||
|
<OciInstance compose={compose} isExpired={isExpired} />
|
||||||
|
);
|
||||||
const status = (
|
const status = (
|
||||||
<ExpiringStatus
|
<ExpiringStatus
|
||||||
compose={compose}
|
compose={compose}
|
||||||
|
|
@ -467,18 +471,8 @@ type AwsRowPropTypes = {
|
||||||
|
|
||||||
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const target = <AwsTarget compose={compose} />;
|
const target = <AwsTarget compose={compose} />;
|
||||||
const status = <CloudStatus compose={compose} />;
|
const status = <CloudStatus compose={compose} />;
|
||||||
|
|
@ -553,18 +547,8 @@ const Row = ({
|
||||||
details,
|
details,
|
||||||
instance,
|
instance,
|
||||||
}: RowPropTypes) => {
|
}: RowPropTypes) => {
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const handleToggle = () => setIsExpanded(!isExpanded);
|
const handleToggle = () => setIsExpanded(!isExpanded);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Suspense, useEffect, useState } from 'react';
|
import React, { Suspense, useState } from 'react';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
} from '@patternfly/react-core/dist/esm/components/List/List';
|
} from '@patternfly/react-core/dist/esm/components/List/List';
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
|
||||||
import { useLoadModule, useScalprum } from '@scalprum/react-core';
|
import { useLoadModule, useScalprum } from '@scalprum/react-core';
|
||||||
import cockpit from 'cockpit';
|
import cockpit from 'cockpit';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
@ -31,6 +30,7 @@ import {
|
||||||
MODAL_ANCHOR,
|
MODAL_ANCHOR,
|
||||||
SEARCH_INPUT,
|
SEARCH_INPUT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import { useGetUser } from '../../Hooks';
|
||||||
import {
|
import {
|
||||||
useGetBlueprintsQuery,
|
useGetBlueprintsQuery,
|
||||||
useGetComposeStatusQuery,
|
useGetComposeStatusQuery,
|
||||||
|
|
@ -101,19 +101,9 @@ const ProvisioningLink = ({
|
||||||
composeStatus,
|
composeStatus,
|
||||||
}: ProvisioningLinkPropTypes) => {
|
}: ProvisioningLinkPropTypes) => {
|
||||||
const launchEofFlag = useFlag('image-builder.launcheof');
|
const launchEofFlag = useFlag('image-builder.launcheof');
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [exposedScalprumModule, error] = useLoadModule(
|
const [exposedScalprumModule, error] = useLoadModule(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import './ImageBuildStatus.scss';
|
import './ImageBuildStatus.scss';
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,13 +24,13 @@ import {
|
||||||
PendingIcon,
|
PendingIcon,
|
||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
import { ChromeUser } from '@redhat-cloud-services/types';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AMPLITUDE_MODULE_NAME,
|
AMPLITUDE_MODULE_NAME,
|
||||||
AWS_S3_EXPIRATION_TIME_IN_HOURS,
|
AWS_S3_EXPIRATION_TIME_IN_HOURS,
|
||||||
OCI_STORAGE_EXPIRATION_TIME_IN_DAYS,
|
OCI_STORAGE_EXPIRATION_TIME_IN_DAYS,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import { useGetUser } from '../../Hooks';
|
||||||
import { useGetComposeStatusQuery } from '../../store/backendApi';
|
import { useGetComposeStatusQuery } from '../../store/backendApi';
|
||||||
import { CockpitComposesResponseItem } from '../../store/cockpit/types';
|
import { CockpitComposesResponseItem } from '../../store/cockpit/types';
|
||||||
import {
|
import {
|
||||||
|
|
@ -122,18 +122,8 @@ export const CloudStatus = ({ compose }: CloudStatusPropTypes) => {
|
||||||
const { data, isSuccess } = useGetComposeStatusQuery({
|
const { data, isSuccess } = useGetComposeStatusQuery({
|
||||||
composeId: compose.id,
|
composeId: compose.id,
|
||||||
});
|
});
|
||||||
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
|
const { userData } = useGetUser(auth);
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const data = await auth.getUser();
|
|
||||||
setUserData(data);
|
|
||||||
})();
|
|
||||||
// This useEffect hook should run *only* on mount and therefore has an empty
|
|
||||||
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
return <Skeleton />;
|
return <Skeleton />;
|
||||||
|
|
|
||||||
116
src/Components/Launch/AzureLaunchModal.tsx
Normal file
116
src/Components/Launch/AzureLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import React, { Fragment, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
ListComponent,
|
||||||
|
ListItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalVariant,
|
||||||
|
OrderType,
|
||||||
|
Skeleton,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ComposesResponseItem,
|
||||||
|
useGetComposeStatusQuery,
|
||||||
|
} from '../../store/imageBuilderApi';
|
||||||
|
import { isAzureUploadStatus } from '../../store/typeGuards';
|
||||||
|
|
||||||
|
type LaunchProps = {
|
||||||
|
compose: ComposesResponseItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AzureLaunchModal = ({ compose }: LaunchProps) => {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const { data, isSuccess, isFetching } = useGetComposeStatusQuery({
|
||||||
|
composeId: compose.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isSuccess) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = data?.image_status.upload_status?.options;
|
||||||
|
|
||||||
|
if (options && !isAzureUploadStatus(options)) {
|
||||||
|
throw TypeError(
|
||||||
|
`Error: options must be of type AzureUploadStatus, not ${typeof options}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
|
||||||
|
setIsModalOpen(!isModalOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
isInline
|
||||||
|
isDisabled={data?.image_status.status !== 'success'}
|
||||||
|
onClick={handleModalToggle}
|
||||||
|
>
|
||||||
|
Launch
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleModalToggle}
|
||||||
|
variant={ModalVariant.large}
|
||||||
|
aria-label='Open launch guide wizard'
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
title={'Launch with Microsoft Azure'}
|
||||||
|
labelId='modal-title'
|
||||||
|
description={compose.image_name}
|
||||||
|
/>
|
||||||
|
<ModalBody id='modal-box-body-basic'>
|
||||||
|
<List component={ListComponent.ol} type={OrderType.number}>
|
||||||
|
<ListItem>
|
||||||
|
Locate{' '}
|
||||||
|
{!isFetching && (
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>
|
||||||
|
{options?.image_name}{' '}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isFetching && <Skeleton />}
|
||||||
|
in the{' '}
|
||||||
|
<Button
|
||||||
|
component='a'
|
||||||
|
target='_blank'
|
||||||
|
variant='link'
|
||||||
|
icon={<ExternalLinkAltIcon />}
|
||||||
|
iconPosition='right'
|
||||||
|
href={`https://portal.azure.com/#view/Microsoft_Azure_ComputeHub/ComputeHubMenuBlade/~/imagesBrowse`}
|
||||||
|
className='pf-v6-u-pl-0'
|
||||||
|
>
|
||||||
|
Azure console
|
||||||
|
</Button>
|
||||||
|
.
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
Create a Virtual Machine (VM) by using the image.
|
||||||
|
<br />
|
||||||
|
Note: Review the{' '}
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>
|
||||||
|
Availability Zone
|
||||||
|
</span>{' '}
|
||||||
|
and the <span className='pf-v6-u-font-weight-bold'>Size</span> to
|
||||||
|
meet your requirements. Adjust these settings as needed.
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
139
src/Components/Launch/OciLaunchModal.tsx
Normal file
139
src/Components/Launch/OciLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
import React, { Fragment, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ClipboardCopy,
|
||||||
|
ClipboardCopyVariant,
|
||||||
|
List,
|
||||||
|
ListComponent,
|
||||||
|
ListItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalVariant,
|
||||||
|
OrderType,
|
||||||
|
Skeleton,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ComposesResponseItem,
|
||||||
|
useGetComposeStatusQuery,
|
||||||
|
} from '../../store/imageBuilderApi';
|
||||||
|
import { isOciUploadStatus } from '../../store/typeGuards';
|
||||||
|
import { resolveRelPath } from '../../Utilities/path';
|
||||||
|
|
||||||
|
type LaunchProps = {
|
||||||
|
isExpired: boolean;
|
||||||
|
compose: ComposesResponseItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OciLaunchModal = ({ isExpired, compose }: LaunchProps) => {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const { data, isSuccess, isFetching } = useGetComposeStatusQuery({
|
||||||
|
composeId: compose.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
if (!isSuccess) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = data?.image_status.upload_status?.options;
|
||||||
|
|
||||||
|
if (options && !isOciUploadStatus(options)) {
|
||||||
|
throw TypeError(
|
||||||
|
`Error: options must be of type OciUploadStatus, not ${typeof options}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
component='a'
|
||||||
|
target='_blank'
|
||||||
|
variant='link'
|
||||||
|
onClick={() => navigate(resolveRelPath(`imagewizard/${compose.id}`))}
|
||||||
|
isInline
|
||||||
|
>
|
||||||
|
Recreate image
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalToggle = () => {
|
||||||
|
setIsModalOpen(!isModalOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
isInline
|
||||||
|
isDisabled={data?.image_status.status !== 'success'}
|
||||||
|
onClick={handleModalToggle}
|
||||||
|
>
|
||||||
|
Image link
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleModalToggle}
|
||||||
|
variant={ModalVariant.large}
|
||||||
|
aria-label='Open launch guide modal'
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
title={'Launch with Oracle Cloud Infrastructure'}
|
||||||
|
labelId='modal-title'
|
||||||
|
description={compose.image_name}
|
||||||
|
/>
|
||||||
|
<ModalBody id='modal-box-body-basic'>
|
||||||
|
<List component={ListComponent.ol} type={OrderType.number}>
|
||||||
|
<ListItem>
|
||||||
|
Navigate to the{' '}
|
||||||
|
<Button
|
||||||
|
component='a'
|
||||||
|
target='_blank'
|
||||||
|
variant='link'
|
||||||
|
icon={<ExternalLinkAltIcon />}
|
||||||
|
iconPosition='right'
|
||||||
|
href={`https://cloud.oracle.com/compute/images`}
|
||||||
|
className='pf-v6-u-pl-0'
|
||||||
|
>
|
||||||
|
Oracle Cloud's Custom Images
|
||||||
|
</Button>{' '}
|
||||||
|
page.
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
Select{' '}
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>Import image</span>,
|
||||||
|
and enter the Object Storage URL of the image.
|
||||||
|
{!isFetching && (
|
||||||
|
<ClipboardCopy
|
||||||
|
isReadOnly
|
||||||
|
isExpanded
|
||||||
|
hoverTip='Copy'
|
||||||
|
clickTip='Copied'
|
||||||
|
variant={ClipboardCopyVariant.expansion}
|
||||||
|
>
|
||||||
|
{options?.url || ''}
|
||||||
|
</ClipboardCopy>
|
||||||
|
)}
|
||||||
|
{isFetching && <Skeleton />}
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
After the image is available, click on{' '}
|
||||||
|
<span className='pf-v6-u-font-weight-bold'>Create instance</span>.
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -156,7 +156,7 @@ const RegionsSelect = ({ composeId, handleClose }: RegionsSelectPropTypes) => {
|
||||||
<MenuToggle
|
<MenuToggle
|
||||||
variant='typeahead'
|
variant='typeahead'
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
innerRef={toggleRef}
|
ref={toggleRef}
|
||||||
isExpanded={isOpen}
|
isExpanded={isOpen}
|
||||||
>
|
>
|
||||||
<TextInputGroup isPlain>
|
<TextInputGroup isPlain>
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ export { useDeleteBPWithNotification } from './MutationNotifications/useDeleteBP
|
||||||
export { useFixupBPWithNotification } from './MutationNotifications/useFixupBPWithNotification';
|
export { useFixupBPWithNotification } from './MutationNotifications/useFixupBPWithNotification';
|
||||||
export { useComposeBPWithNotification } from './MutationNotifications/useComposeBPWithNotification';
|
export { useComposeBPWithNotification } from './MutationNotifications/useComposeBPWithNotification';
|
||||||
export { useCloneComposeWithNotification } from './MutationNotifications/useCloneComposeWithNotification';
|
export { useCloneComposeWithNotification } from './MutationNotifications/useCloneComposeWithNotification';
|
||||||
|
export { useGetUser } from './useGetUser';
|
||||||
|
|
|
||||||
24
src/Hooks/useGetUser.tsx
Normal file
24
src/Hooks/useGetUser.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
|
|
||||||
|
export const useGetUser = (auth: { getUser(): Promise<void | ChromeUser> }) => {
|
||||||
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
const [orgId, setOrgId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (!process.env.IS_ON_PREMISE) {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
const id = data?.identity.internal?.org_id;
|
||||||
|
setUserData(data);
|
||||||
|
setOrgId(id);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// This useEffect hook should run *only* on mount and therefore has an empty
|
||||||
|
// dependency array. eslint's exhaustive-deps rule does not support this use.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { userData, orgId };
|
||||||
|
};
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* - Please do NOT modify this file.
|
* - Please do NOT modify this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PACKAGE_VERSION = '2.10.4'
|
const PACKAGE_VERSION = '2.10.5'
|
||||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||||
const activeClientIds = new Set()
|
const activeClientIds = new Set()
|
||||||
|
|
|
||||||
|
|
@ -513,6 +513,123 @@ describe('Step Packages', () => {
|
||||||
expect(secondAppStreamRow).toBeDisabled();
|
expect(secondAppStreamRow).toBeDisabled();
|
||||||
expect(secondAppStreamRow).not.toBeChecked();
|
expect(secondAppStreamRow).not.toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('module selection sorts selected stream to top while maintaining alphabetical order', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
await renderCreateMode();
|
||||||
|
await goToPackagesStep();
|
||||||
|
await typeIntoSearchBox('sortingTest');
|
||||||
|
|
||||||
|
await screen.findAllByText('alphaModule');
|
||||||
|
await screen.findAllByText('betaModule');
|
||||||
|
await screen.findAllByText('gammaModule');
|
||||||
|
|
||||||
|
let rows = await screen.findAllByRole('row');
|
||||||
|
rows.shift();
|
||||||
|
expect(rows).toHaveLength(6);
|
||||||
|
|
||||||
|
expect(rows[0]).toHaveTextContent('alphaModule');
|
||||||
|
expect(rows[0]).toHaveTextContent('3.0');
|
||||||
|
expect(rows[1]).toHaveTextContent('alphaModule');
|
||||||
|
expect(rows[1]).toHaveTextContent('2.0');
|
||||||
|
expect(rows[2]).toHaveTextContent('betaModule');
|
||||||
|
expect(rows[2]).toHaveTextContent('4.0');
|
||||||
|
expect(rows[3]).toHaveTextContent('betaModule');
|
||||||
|
expect(rows[3]).toHaveTextContent('2.0');
|
||||||
|
|
||||||
|
// Select betaModule with stream 2.0 (row index 3)
|
||||||
|
const betaModule20Checkbox = await screen.findByRole('checkbox', {
|
||||||
|
name: /select row 3/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => user.click(betaModule20Checkbox));
|
||||||
|
expect(betaModule20Checkbox).toBeChecked();
|
||||||
|
|
||||||
|
// After selection, the active stream (2.0) should be prioritized
|
||||||
|
// All modules with stream 2.0 should move to the top, maintaining alphabetical order
|
||||||
|
rows = await screen.findAllByRole('row');
|
||||||
|
rows.shift();
|
||||||
|
expect(rows[0]).toHaveTextContent('alphaModule');
|
||||||
|
expect(rows[0]).toHaveTextContent('2.0');
|
||||||
|
expect(rows[1]).toHaveTextContent('betaModule');
|
||||||
|
expect(rows[1]).toHaveTextContent('2.0');
|
||||||
|
expect(rows[2]).toHaveTextContent('gammaModule');
|
||||||
|
expect(rows[2]).toHaveTextContent('2.0');
|
||||||
|
expect(rows[3]).toHaveTextContent('alphaModule');
|
||||||
|
expect(rows[3]).toHaveTextContent('3.0');
|
||||||
|
expect(rows[4]).toHaveTextContent('betaModule');
|
||||||
|
expect(rows[4]).toHaveTextContent('4.0');
|
||||||
|
expect(rows[5]).toHaveTextContent('gammaModule');
|
||||||
|
expect(rows[5]).toHaveTextContent('1.5');
|
||||||
|
|
||||||
|
// Verify that only the selected module is checked
|
||||||
|
const updatedBetaModule20Checkbox = await screen.findByRole('checkbox', {
|
||||||
|
name: /select row 1/i, // betaModule 2.0 is now at position 1
|
||||||
|
});
|
||||||
|
expect(updatedBetaModule20Checkbox).toBeChecked();
|
||||||
|
|
||||||
|
// Verify that only one checkbox is checked
|
||||||
|
const allCheckboxes = await screen.findAllByRole('checkbox', {
|
||||||
|
name: /select row [0-9]/i,
|
||||||
|
});
|
||||||
|
const checkedCheckboxes = allCheckboxes.filter(
|
||||||
|
(cb) => (cb as HTMLInputElement).checked,
|
||||||
|
);
|
||||||
|
expect(checkedCheckboxes).toHaveLength(1);
|
||||||
|
expect(checkedCheckboxes[0]).toBe(updatedBetaModule20Checkbox);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unselecting a module does not cause jumping but may reset sort to default', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
await renderCreateMode();
|
||||||
|
await goToPackagesStep();
|
||||||
|
await selectCustomRepo();
|
||||||
|
await typeIntoSearchBox('sortingTest');
|
||||||
|
await screen.findAllByText('betaModule');
|
||||||
|
const betaModule20Checkbox = await screen.findByRole('checkbox', {
|
||||||
|
name: /select row 3/i,
|
||||||
|
});
|
||||||
|
await waitFor(() => user.click(betaModule20Checkbox));
|
||||||
|
expect(betaModule20Checkbox).toBeChecked();
|
||||||
|
let rows = await screen.findAllByRole('row');
|
||||||
|
rows.shift();
|
||||||
|
expect(rows[0]).toHaveTextContent('alphaModule');
|
||||||
|
expect(rows[0]).toHaveTextContent('2.0');
|
||||||
|
expect(rows[1]).toHaveTextContent('betaModule');
|
||||||
|
expect(rows[1]).toHaveTextContent('2.0');
|
||||||
|
|
||||||
|
const updatedBetaModule20Checkbox = await screen.findByRole('checkbox', {
|
||||||
|
name: /select row 1/i,
|
||||||
|
});
|
||||||
|
await waitFor(() => user.click(updatedBetaModule20Checkbox));
|
||||||
|
expect(updatedBetaModule20Checkbox).not.toBeChecked();
|
||||||
|
|
||||||
|
// After unselection, the sort may reset to default or stay the same
|
||||||
|
// The important thing is that we don't get jumping/reordering during the interaction
|
||||||
|
rows = await screen.findAllByRole('row');
|
||||||
|
rows.shift(); // Remove header row
|
||||||
|
const allCheckboxes = await screen.findAllByRole('checkbox', {
|
||||||
|
name: /select row [0-9]/i,
|
||||||
|
});
|
||||||
|
const checkedCheckboxes = allCheckboxes.filter(
|
||||||
|
(cb) => (cb as HTMLInputElement).checked,
|
||||||
|
);
|
||||||
|
expect(checkedCheckboxes).toHaveLength(0);
|
||||||
|
|
||||||
|
// The key test: the table should have a consistent, predictable order
|
||||||
|
// Either the original alphabetical order OR the stream-sorted order
|
||||||
|
// What we don't want is jumping around during the selection/unselection process
|
||||||
|
expect(rows).toHaveLength(6); // Still have all 6 modules
|
||||||
|
const moduleNames = rows.map((row) => {
|
||||||
|
const match = row.textContent?.match(/(\w+Module)/);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
});
|
||||||
|
expect(moduleNames).toContain('alphaModule');
|
||||||
|
expect(moduleNames).toContain('betaModule');
|
||||||
|
expect(moduleNames).toContain('gammaModule');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
58
src/test/fixtures/packages.ts
vendored
58
src/test/fixtures/packages.ts
vendored
|
|
@ -75,6 +75,64 @@ export const mockSourcesPackagesResults = (
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (search === 'sortingTest') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
package_name: 'alphaModule',
|
||||||
|
summary: 'Alpha module for sorting tests',
|
||||||
|
package_sources: [
|
||||||
|
{
|
||||||
|
name: 'alphaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '2.0',
|
||||||
|
end_date: '2025-12-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'alphaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '3.0',
|
||||||
|
end_date: '2027-12-01',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
package_name: 'betaModule',
|
||||||
|
summary: 'Beta module for sorting tests',
|
||||||
|
package_sources: [
|
||||||
|
{
|
||||||
|
name: 'betaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '2.0',
|
||||||
|
end_date: '2025-06-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'betaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '4.0',
|
||||||
|
end_date: '2028-06-01',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
package_name: 'gammaModule',
|
||||||
|
summary: 'Gamma module for sorting tests',
|
||||||
|
package_sources: [
|
||||||
|
{
|
||||||
|
name: 'gammaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '2.0',
|
||||||
|
end_date: '2025-08-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gammaModule',
|
||||||
|
type: 'module',
|
||||||
|
stream: '1.5',
|
||||||
|
end_date: '2026-08-01',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
if (search === 'mock') {
|
if (search === 'mock') {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue