Compare commits

..

No commits in common. "main" and "v74" have entirely different histories.
main ... v74

37 changed files with 1258 additions and 1291 deletions

View file

@ -1 +0,0 @@
1

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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, '');
};

View file

@ -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();
}; };

View file

@ -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();
}); });

View file

@ -1,8 +0,0 @@
summary: run playwright tests
test: ./playwright_tests.sh
require:
- cockpit-image-builder
- podman
- nodejs
- nodejs-npm
duration: 30m

View file

@ -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 \

View file

@ -1 +1 @@
cf0a810fd3b75fa27139746c4dfe72222e13dcba 7b4735d287dd0950e0a6f47dde65b62b0f239da1

View file

@ -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>

View file

@ -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;
} }

View file

@ -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) {

View file

@ -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,

View file

@ -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'>

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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, '');
}; };

View file

@ -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,

View file

@ -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(

View file

@ -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);

View file

@ -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(

View file

@ -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 />;

View file

@ -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>
);
};

View file

@ -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&apos;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>
);
};

View file

@ -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>

View file

@ -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';

View file

@ -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 };
};

View file

@ -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()

View file

@ -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');
});
}); });
}); });

View file

@ -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 [
{ {