Compare commits
No commits in common. "main" and "v74" have entirely different histories.
37 changed files with 1258 additions and 1291 deletions
|
|
@ -1 +0,0 @@
|
||||||
1
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
---
|
|
||||||
name: Debian Image Builder Frontend CI/CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, develop]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_VERSION: "18"
|
|
||||||
DEBIAN_FRONTEND: noninteractive
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-test:
|
|
||||||
name: Build and Test Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build || echo "Build script not found"
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run test; then
|
|
||||||
npm test
|
|
||||||
else
|
|
||||||
echo "No test script found, skipping tests"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run linting
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run lint; then
|
|
||||||
npm run lint
|
|
||||||
else
|
|
||||||
echo "No lint script found, skipping linting"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: frontend-build
|
|
||||||
path: |
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
package:
|
|
||||||
name: Package Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
devscripts \
|
|
||||||
debhelper \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create debian directory
|
|
||||||
run: |
|
|
||||||
mkdir -p debian
|
|
||||||
cat > debian/control << EOF
|
|
||||||
Source: debian-image-builder-frontend
|
|
||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Debian Forge Team <team@debian-forge.org>
|
|
||||||
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
|
||||||
Standards-Version: 4.6.2
|
|
||||||
|
|
||||||
Package: debian-image-builder-frontend
|
|
||||||
Architecture: all
|
|
||||||
Depends: \${misc:Depends}, nodejs, nginx
|
|
||||||
Description: Debian Image Builder Frontend
|
|
||||||
Web-based frontend for Debian Image Builder with Cockpit integration.
|
|
||||||
Provides a user interface for managing image builds, blueprints,
|
|
||||||
and system configurations through a modern React application.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/rules << EOF
|
|
||||||
#!/usr/bin/make -f
|
|
||||||
%:
|
|
||||||
dh \$@
|
|
||||||
|
|
||||||
override_dh_auto_install:
|
|
||||||
dh_auto_install
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
|
||||||
|
|
||||||
# Copy built frontend files
|
|
||||||
if [ -d dist ]; then
|
|
||||||
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
elif [ -d build ]; then
|
|
||||||
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy source files for development
|
|
||||||
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
|
|
||||||
# Create nginx configuration
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /usr/share/debian-image-builder-frontend;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://localhost:8080/;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX_EOF
|
|
||||||
|
|
||||||
# Create cockpit manifest
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
|
||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"manifest": {
|
|
||||||
"name": "debian-image-builder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"title": "Debian Image Builder",
|
|
||||||
"description": "Build and manage Debian atomic images",
|
|
||||||
"url": "/usr/share/debian-image-builder-frontend",
|
|
||||||
"icon": "debian-logo",
|
|
||||||
"requires": {
|
|
||||||
"cockpit": ">= 200"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
COCKPIT_EOF
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/changelog << EOF
|
|
||||||
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Initial release
|
|
||||||
* Debian Image Builder Frontend with Cockpit integration
|
|
||||||
* React-based web interface for image management
|
|
||||||
|
|
||||||
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/compat << EOF
|
|
||||||
13
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x debian/rules
|
|
||||||
|
|
||||||
- name: Build Debian package
|
|
||||||
run: |
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
ls -la ../*.deb
|
|
||||||
|
|
||||||
- name: Upload Debian package
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: debian-image-builder-frontend-deb
|
|
||||||
path: ../*.deb
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
cockpit-integration:
|
|
||||||
name: Test Cockpit Integration
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Test cockpit integration
|
|
||||||
run: |
|
|
||||||
echo "Testing Cockpit integration..."
|
|
||||||
if [ -d cockpit ]; then
|
|
||||||
echo "Cockpit directory found:"
|
|
||||||
ls -la cockpit/
|
|
||||||
else
|
|
||||||
echo "No cockpit directory found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f package.json ]; then
|
|
||||||
echo "Package.json scripts:"
|
|
||||||
npm run
|
|
||||||
fi
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
---
|
|
||||||
name: Debian Image Builder Frontend CI/CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, develop]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_VERSION: "18"
|
|
||||||
DEBIAN_FRONTEND: noninteractive
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-test:
|
|
||||||
name: Build and Test Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build || echo "Build script not found"
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run test; then
|
|
||||||
npm test
|
|
||||||
else
|
|
||||||
echo "No test script found, skipping tests"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run linting
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run lint; then
|
|
||||||
npm run lint
|
|
||||||
else
|
|
||||||
echo "No lint script found, skipping linting"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: frontend-build
|
|
||||||
path: |
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
package:
|
|
||||||
name: Package Frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
devscripts \
|
|
||||||
debhelper \
|
|
||||||
git \
|
|
||||||
ca-certificates \
|
|
||||||
python3
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build production bundle
|
|
||||||
run: |
|
|
||||||
if [ -f package.json ] && npm run build; then
|
|
||||||
npm run build
|
|
||||||
else
|
|
||||||
echo "No build script found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create debian directory
|
|
||||||
run: |
|
|
||||||
mkdir -p debian
|
|
||||||
cat > debian/control << EOF
|
|
||||||
Source: debian-image-builder-frontend
|
|
||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Debian Forge Team <team@debian-forge.org>
|
|
||||||
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
|
|
||||||
Standards-Version: 4.6.2
|
|
||||||
|
|
||||||
Package: debian-image-builder-frontend
|
|
||||||
Architecture: all
|
|
||||||
Depends: \${misc:Depends}, nodejs, nginx
|
|
||||||
Description: Debian Image Builder Frontend
|
|
||||||
Web-based frontend for Debian Image Builder with Cockpit integration.
|
|
||||||
Provides a user interface for managing image builds, blueprints,
|
|
||||||
and system configurations through a modern React application.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/rules << EOF
|
|
||||||
#!/usr/bin/make -f
|
|
||||||
%:
|
|
||||||
dh \$@
|
|
||||||
|
|
||||||
override_dh_auto_install:
|
|
||||||
dh_auto_install
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
|
|
||||||
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
|
|
||||||
|
|
||||||
# Copy built frontend files
|
|
||||||
if [ -d dist ]; then
|
|
||||||
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
elif [ -d build ]; then
|
|
||||||
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy source files for development
|
|
||||||
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
|
|
||||||
|
|
||||||
# Create nginx configuration
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
root /usr/share/debian-image-builder-frontend;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://localhost:8080/;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX_EOF
|
|
||||||
|
|
||||||
# Create cockpit manifest
|
|
||||||
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
|
|
||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"manifest": {
|
|
||||||
"name": "debian-image-builder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"title": "Debian Image Builder",
|
|
||||||
"description": "Build and manage Debian atomic images",
|
|
||||||
"url": "/usr/share/debian-image-builder-frontend",
|
|
||||||
"icon": "debian-logo",
|
|
||||||
"requires": {
|
|
||||||
"cockpit": ">= 200"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
COCKPIT_EOF
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/changelog << EOF
|
|
||||||
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Initial release
|
|
||||||
* Debian Image Builder Frontend with Cockpit integration
|
|
||||||
* React-based web interface for image management
|
|
||||||
|
|
||||||
-- Debian Forge Team <team@debian-forge.org> $(date -R)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > debian/compat << EOF
|
|
||||||
13
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x debian/rules
|
|
||||||
|
|
||||||
- name: Build Debian package
|
|
||||||
run: |
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
ls -la ../*.deb
|
|
||||||
|
|
||||||
- name: Upload Debian package
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: debian-image-builder-frontend-deb
|
|
||||||
path: ../*.deb
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
cockpit-integration:
|
|
||||||
name: Test Cockpit Integration
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:18-bullseye
|
|
||||||
needs: build-and-test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
run: |
|
|
||||||
node --version
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Test cockpit integration
|
|
||||||
run: |
|
|
||||||
echo "Testing Cockpit integration..."
|
|
||||||
if [ -d cockpit ]; then
|
|
||||||
echo "Cockpit directory found:"
|
|
||||||
ls -la cockpit/
|
|
||||||
else
|
|
||||||
echo "No cockpit directory found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f package.json ]; then
|
|
||||||
echo "Package.json scripts:"
|
|
||||||
npm run
|
|
||||||
fi
|
|
||||||
|
|
@ -32,7 +32,8 @@ test:
|
||||||
- RUNNER:
|
- RUNNER:
|
||||||
- aws/fedora-41-x86_64
|
- aws/fedora-41-x86_64
|
||||||
- aws/fedora-42-x86_64
|
- aws/fedora-42-x86_64
|
||||||
- aws/rhel-10.1-nightly-x86_64
|
- aws/rhel-9.6-nightly-x86_64
|
||||||
|
- aws/rhel-10.0-nightly-x86_64
|
||||||
INTERNAL_NETWORK: ["true"]
|
INTERNAL_NETWORK: ["true"]
|
||||||
|
|
||||||
finish:
|
finish:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Name: cockpit-image-builder
|
Name: cockpit-image-builder
|
||||||
Version: 76
|
Version: 74
|
||||||
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.3.0",
|
"@types/node": "24.1.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.40.0",
|
"@typescript-eslint/eslint-plugin": "8.39.1",
|
||||||
"@typescript-eslint/parser": "8.40.0",
|
"@typescript-eslint/parser": "8.39.1",
|
||||||
"@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.5",
|
"msw": "2.10.4",
|
||||||
"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,15 +16,6 @@ srpm_build_deps:
|
||||||
- npm
|
- npm
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: tests
|
|
||||||
identifier: self
|
|
||||||
trigger: pull_request
|
|
||||||
tmt_plan: /plans/all/main
|
|
||||||
targets:
|
|
||||||
- centos-stream-10
|
|
||||||
- fedora-41
|
|
||||||
- fedora-42
|
|
||||||
|
|
||||||
- job: copr_build
|
- job: copr_build
|
||||||
trigger: pull_request
|
trigger: pull_request
|
||||||
targets: &build_targets
|
targets: &build_targets
|
||||||
|
|
@ -33,6 +24,7 @@ jobs:
|
||||||
- centos-stream-10
|
- centos-stream-10
|
||||||
- centos-stream-10-aarch64
|
- centos-stream-10-aarch64
|
||||||
- fedora-all
|
- fedora-all
|
||||||
|
- fedora-all-aarch64
|
||||||
|
|
||||||
- job: copr_build
|
- job: copr_build
|
||||||
trigger: commit
|
trigger: commit
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
summary: cockpit-image-builder playwright tests
|
|
||||||
prepare:
|
|
||||||
how: install
|
|
||||||
package:
|
|
||||||
- cockpit-image-builder
|
|
||||||
discover:
|
|
||||||
how: fmf
|
|
||||||
execute:
|
|
||||||
how: tmt
|
|
||||||
|
|
||||||
/main:
|
|
||||||
summary: playwright tests
|
|
||||||
discover+:
|
|
||||||
test: /schutzbot/playwright
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
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) => {
|
||||||
|
|
@ -45,43 +42,3 @@ 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 { expect, FrameLocator, Page } from '@playwright/test';
|
import type { FrameLocator, Page } from '@playwright/test';
|
||||||
|
|
||||||
import { getHostArch, getHostDistroName, isHosted } from './helpers';
|
import { isHosted } from './helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the wizard, fills out the "Image Output" step, and navigates to the optional steps
|
* Opens the wizard, fills out the "Image Output" step, and navigates to the optional steps
|
||||||
|
|
@ -8,13 +8,6 @@ import { getHostArch, getHostDistroName, 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,12 +92,7 @@ test.describe.serial('test', () => {
|
||||||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
frame.locator('.pf-v6-c-card__title-text').getByText(
|
frame.locator('.pf-v6-c-card__title-text').getByText(blueprintName),
|
||||||
// if the name is too long, the blueprint card will have a truncated name.
|
|
||||||
blueprintName.length > 24
|
|
||||||
? blueprintName.slice(0, 24) + '...'
|
|
||||||
: blueprintName,
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
summary: run playwright tests
|
|
||||||
test: ./playwright_tests.sh
|
|
||||||
require:
|
|
||||||
- cockpit-image-builder
|
|
||||||
- podman
|
|
||||||
- nodejs
|
|
||||||
- nodejs-npm
|
|
||||||
duration: 30m
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
TMT_SOURCE_DIR=${TMT_SOURCE_DIR:-}
|
# As playwright isn't supported on fedora/el, install dependencies
|
||||||
if [ -n "$TMT_SOURCE_DIR" ]; then
|
# beforehand.
|
||||||
# Move to the directory with sources
|
sudo dnf install -y \
|
||||||
cd "${TMT_SOURCE_DIR}/cockpit-image-builder"
|
alsa-lib \
|
||||||
npm ci
|
libXrandr-devel \
|
||||||
elif [ "${CI:-}" != "true" ]; then
|
libXdamage-devel \
|
||||||
# packit drops us into the schutzbot directory
|
libXcomposite-devel \
|
||||||
cd ../
|
at-spi2-atk-devel \
|
||||||
npm ci
|
cups \
|
||||||
fi
|
atk
|
||||||
|
|
||||||
sudo systemctl enable --now cockpit.socket
|
sudo systemctl enable --now cockpit.socket
|
||||||
|
|
||||||
|
|
@ -19,13 +19,10 @@ sudo usermod -aG wheel admin
|
||||||
echo "admin ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/admin-nopasswd"
|
echo "admin ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/admin-nopasswd"
|
||||||
|
|
||||||
function upload_artifacts {
|
function upload_artifacts {
|
||||||
if [ -n "${TMT_TEST_DATA:-}" ]; then
|
mkdir -p /tmp/artifacts/extra-screenshots
|
||||||
mv playwright-report "$TMT_TEST_DATA"/playwright-report
|
USER="$(whoami)"
|
||||||
else
|
sudo chown -R "$USER:$USER" playwright-report
|
||||||
USER="$(whoami)"
|
mv playwright-report /tmp/artifacts/
|
||||||
sudo chown -R "$USER:$USER" playwright-report
|
|
||||||
mv playwright-report /tmp/artifacts/
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
trap upload_artifacts EXIT
|
trap upload_artifacts EXIT
|
||||||
|
|
||||||
|
|
@ -76,12 +73,11 @@ 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 @@
|
||||||
cf0a810fd3b75fa27139746c4dfe72222e13dcba
|
7b4735d287dd0950e0a6f47dde65b62b0f239da1
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ 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';
|
||||||
|
|
@ -50,21 +51,11 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
|
||||||
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
onChange: () => dispatch(setBlueprintId(blueprint.id)),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardTitle aria-label={blueprint.name}>
|
<CardTitle>
|
||||||
{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 } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Bullseye,
|
Bullseye,
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons';
|
||||||
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
|
||||||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||||
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
@ -28,7 +29,6 @@ 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,6 +73,16 @@ 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, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -16,13 +16,11 @@ 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 {
|
import { useComposeBPWithNotification as useComposeBlueprintMutation } from '../../Hooks';
|
||||||
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';
|
||||||
|
|
@ -39,7 +37,18 @@ 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 from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -9,16 +9,14 @@ 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 {
|
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
|
||||||
useDeleteBPWithNotification as useDeleteBlueprintMutation,
|
|
||||||
useGetUser,
|
|
||||||
} from '../../Hooks';
|
|
||||||
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
|
||||||
import {
|
import {
|
||||||
selectBlueprintSearchInput,
|
selectBlueprintSearchInput,
|
||||||
|
|
@ -44,7 +42,17 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
||||||
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { analytics, auth } = useChrome();
|
const { analytics, auth } = useChrome();
|
||||||
const { userData } = useGetUser(auth);
|
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const data = await auth.getUser();
|
||||||
|
setUserData(data);
|
||||||
|
})();
|
||||||
|
// 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,7 +50,6 @@ 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';
|
||||||
|
|
@ -67,6 +66,7 @@ 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 = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let unpackedData: IBPackageWithRepositoryInfo[] =
|
const 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,16 +724,13 @@ 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 = orderBy(
|
unpackedData.sort((a, b) => {
|
||||||
unpackedData,
|
if (a.name === b.name) {
|
||||||
[
|
return (b.stream ?? '').localeCompare(a.stream ?? '');
|
||||||
'name',
|
} else {
|
||||||
(pkg) => pkg.stream || '',
|
return a.name.localeCompare(b.name);
|
||||||
(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) {
|
||||||
|
|
@ -869,6 +866,8 @@ 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 || '',
|
||||||
|
|
@ -994,18 +993,7 @@ const Packages = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPackageUniqueKey = (pkg: IBPackageWithRepositoryInfo): string => {
|
const initialExpandedPkgs: IBPackageWithRepositoryInfo[] = [];
|
||||||
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 = (
|
||||||
|
|
@ -1013,13 +1001,12 @@ const Packages = () => {
|
||||||
isExpanding: boolean,
|
isExpanding: boolean,
|
||||||
) =>
|
) =>
|
||||||
setExpandedPkgs((prevExpanded) => {
|
setExpandedPkgs((prevExpanded) => {
|
||||||
const pkgKey = getPackageUniqueKey(pkg);
|
const otherExpandedPkgs = prevExpanded.filter((p) => p.name !== pkg.name);
|
||||||
const otherExpandedPkgs = prevExpanded.filter((key) => key !== pkgKey);
|
return isExpanding ? [...otherExpandedPkgs, pkg] : otherExpandedPkgs;
|
||||||
return isExpanding ? [...otherExpandedPkgs, pkgKey] : otherExpandedPkgs;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
||||||
expandedPkgs.includes(getPackageUniqueKey(pkg));
|
expandedPkgs.includes(pkg);
|
||||||
|
|
||||||
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
||||||
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
||||||
|
|
@ -1043,37 +1030,51 @@ const Packages = () => {
|
||||||
'asc' | 'desc'
|
'asc' | 'desc'
|
||||||
>('asc');
|
>('asc');
|
||||||
|
|
||||||
const sortedPackages = useMemo(() => {
|
const getSortableRowValues = (
|
||||||
if (!transformedPackages || !Array.isArray(transformedPackages)) {
|
pkg: IBPackageWithRepositoryInfo,
|
||||||
return [];
|
): (string | number | ApiPackageSourcesResponse[] | undefined)[] => {
|
||||||
}
|
return [pkg.name, pkg.summary, pkg.stream, pkg.end_date, pkg.repository];
|
||||||
|
};
|
||||||
|
|
||||||
return orderBy(
|
let sortedPackages = transformedPackages;
|
||||||
transformedPackages,
|
sortedPackages = transformedPackages.sort((a, b) => {
|
||||||
[
|
const aValue = getSortableRowValues(a)[activeSortIndex];
|
||||||
// Active stream packages first (if activeStream is set)
|
const bValue = getSortableRowValues(b)[activeSortIndex];
|
||||||
(pkg) => (activeStream && pkg.stream === activeStream ? 0 : 1),
|
if (typeof aValue === 'number') {
|
||||||
// Then by name
|
// Numeric sort
|
||||||
'name',
|
if (activeSortDirection === 'asc') {
|
||||||
// Then by stream version (descending)
|
return (aValue as number) - (bValue as number);
|
||||||
(pkg) => {
|
}
|
||||||
if (!pkg.stream) return '';
|
return (bValue as number) - (aValue as number);
|
||||||
const parts = pkg.stream
|
}
|
||||||
.split('.')
|
// String sort
|
||||||
.map((part) => parseInt(part, 10) || 0);
|
// if active stream is set, sort it to the top
|
||||||
// Convert to string with zero-padding for proper sorting
|
if (aValue === activeStream) {
|
||||||
return parts.map((p) => p.toString().padStart(10, '0')).join('.');
|
return -1;
|
||||||
},
|
}
|
||||||
// Then by end date (nulls last)
|
if (bValue === activeStream) {
|
||||||
(pkg) => pkg.end_date || '9999-12-31',
|
return 1;
|
||||||
// Then by repository
|
}
|
||||||
(pkg) => pkg.repository || '',
|
if (activeSortDirection === 'asc') {
|
||||||
// Finally by module name
|
// handle packages with undefined stream
|
||||||
(pkg) => pkg.module_name || '',
|
if (!aValue) {
|
||||||
],
|
return -1;
|
||||||
['asc', 'asc', 'desc', 'asc', 'asc', 'asc'],
|
}
|
||||||
);
|
if (!bValue) {
|
||||||
}, [transformedPackages, activeStream]);
|
return 1;
|
||||||
|
}
|
||||||
|
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: {
|
||||||
|
|
@ -1099,14 +1100,14 @@ const Packages = () => {
|
||||||
(module) => module.name === pkg.name,
|
(module) => module.name === pkg.name,
|
||||||
);
|
);
|
||||||
isSelected =
|
isSelected =
|
||||||
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
packages.some((p) => p.name === pkg.name) && !isModuleWithSameName;
|
||||||
!isModuleWithSameName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pkg.type === 'module') {
|
if (pkg.type === 'module') {
|
||||||
// the package is selected if its module stream matches one in enabled_modules
|
// the package is selected if it's added to the packages state
|
||||||
|
// and its module stream matches one in enabled_modules
|
||||||
isSelected =
|
isSelected =
|
||||||
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
|
packages.some((p) => p.name === pkg.name) &&
|
||||||
modules.some(
|
modules.some(
|
||||||
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
|
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
|
||||||
);
|
);
|
||||||
|
|
@ -1207,7 +1208,7 @@ const Packages = () => {
|
||||||
.slice(computeStart(), computeEnd())
|
.slice(computeStart(), computeEnd())
|
||||||
.map((grp, rowIndex) => (
|
.map((grp, rowIndex) => (
|
||||||
<Tbody
|
<Tbody
|
||||||
key={`${grp.name}-${grp.repository || 'default'}`}
|
key={`${grp.name}-${rowIndex}`}
|
||||||
isExpanded={isGroupExpanded(grp.name)}
|
isExpanded={isGroupExpanded(grp.name)}
|
||||||
>
|
>
|
||||||
<Tr data-testid='package-row'>
|
<Tr data-testid='package-row'>
|
||||||
|
|
@ -1307,7 +1308,7 @@ const Packages = () => {
|
||||||
.slice(computeStart(), computeEnd())
|
.slice(computeStart(), computeEnd())
|
||||||
.map((pkg, rowIndex) => (
|
.map((pkg, rowIndex) => (
|
||||||
<Tbody
|
<Tbody
|
||||||
key={`${pkg.name}-${pkg.stream || 'default'}-${pkg.module_name || pkg.name}`}
|
key={`${pkg.name}-${rowIndex}`}
|
||||||
isExpanded={isPkgExpanded(pkg)}
|
isExpanded={isPkgExpanded(pkg)}
|
||||||
>
|
>
|
||||||
<Tr data-testid='package-row'>
|
<Tr data-testid='package-row'>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopy,
|
ClipboardCopy,
|
||||||
|
|
@ -16,7 +16,6 @@ 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,
|
||||||
|
|
@ -25,7 +24,18 @@ import {
|
||||||
|
|
||||||
const RegistrationStep = () => {
|
const RegistrationStep = () => {
|
||||||
const { auth } = useChrome();
|
const { auth } = useChrome();
|
||||||
const { orgId } = useGetUser(auth);
|
const [orgId, setOrgId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
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, { useState } from 'react';
|
import React, { useEffect, 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,8 +44,19 @@ 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);
|
||||||
|
|
||||||
|
|
@ -102,7 +113,17 @@ export const CreateSaveButton = ({
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}: CreateDropdownProps) => {
|
}: CreateDropdownProps) => {
|
||||||
const { analytics, auth, isBeta } = useChrome();
|
const { analytics, auth, isBeta } = 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 packages = useAppSelector(selectPackages);
|
const packages = useAppSelector(selectPackages);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } 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,8 +37,19 @@ 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);
|
||||||
|
|
@ -94,8 +105,19 @@ 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,7 +18,6 @@ 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';
|
||||||
|
|
@ -34,7 +33,6 @@ 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();
|
||||||
|
|
@ -54,12 +52,14 @@ 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 is fine for on prem because we save the org id
|
// NOTE: This should be fine on-prem, we should
|
||||||
// to state through a form field in the registration step
|
// be able to ignore the `org-id`
|
||||||
return mapRequestFromState(store, '');
|
return mapRequestFromState(store, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,9 +211,8 @@ 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 & { region?: string | undefined };
|
.options as AwsUploadRequestOptions;
|
||||||
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
|
||||||
|
|
@ -316,7 +315,6 @@ 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 from 'react';
|
import React, { useEffect, useState } 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,9 +134,19 @@ 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 { useFlag } from '@unleash/proxy-client-react';
|
import { ChromeUser } from '@redhat-cloud-services/types';
|
||||||
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,7 +58,6 @@ 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,
|
||||||
|
|
@ -88,12 +87,11 @@ 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 =
|
||||||
|
|
@ -106,7 +104,16 @@ 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,
|
||||||
|
|
@ -375,14 +382,8 @@ 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 = launchEofFlag ? (
|
const instance = <CloudInstance compose={compose} />;
|
||||||
<AzureLaunchModal compose={compose} />
|
|
||||||
) : (
|
|
||||||
<CloudInstance compose={compose} />
|
|
||||||
);
|
|
||||||
const status = <CloudStatus compose={compose} />;
|
const status = <CloudStatus compose={compose} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -402,18 +403,13 @@ 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 = launchEofFlag ? (
|
const instance = <OciInstance compose={compose} isExpired={isExpired} />;
|
||||||
<OciLaunchModal compose={compose} isExpired={isExpired} />
|
|
||||||
) : (
|
|
||||||
<OciInstance compose={compose} isExpired={isExpired} />
|
|
||||||
);
|
|
||||||
const status = (
|
const status = (
|
||||||
<ExpiringStatus
|
<ExpiringStatus
|
||||||
compose={compose}
|
compose={compose}
|
||||||
|
|
@ -471,8 +467,18 @@ 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} />;
|
||||||
|
|
@ -547,8 +553,18 @@ 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, useState } from 'react';
|
import React, { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
|
@ -20,6 +20,7 @@ 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';
|
||||||
|
|
@ -30,7 +31,6 @@ import {
|
||||||
MODAL_ANCHOR,
|
MODAL_ANCHOR,
|
||||||
SEARCH_INPUT,
|
SEARCH_INPUT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useGetUser } from '../../Hooks';
|
|
||||||
import {
|
import {
|
||||||
useGetBlueprintsQuery,
|
useGetBlueprintsQuery,
|
||||||
useGetComposeStatusQuery,
|
useGetComposeStatusQuery,
|
||||||
|
|
@ -101,9 +101,19 @@ 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 from 'react';
|
import React, { useEffect, useState } 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,8 +122,18 @@ 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 />;
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
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}
|
||||||
ref={toggleRef}
|
innerRef={toggleRef}
|
||||||
isExpanded={isOpen}
|
isExpanded={isOpen}
|
||||||
>
|
>
|
||||||
<TextInputGroup isPlain>
|
<TextInputGroup isPlain>
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,3 @@ 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';
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
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.5'
|
const PACKAGE_VERSION = '2.10.4'
|
||||||
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,123 +513,6 @@ 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,64 +75,6 @@ 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