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