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:
- aws/fedora-41-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"]
finish:

View file

@ -1,5 +1,5 @@
Name: cockpit-image-builder
Version: 76
Version: 74
Release: 1%{?dist}
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/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/node": "24.3.0",
"@types/node": "24.1.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.40.0",
"@typescript-eslint/parser": "8.40.0",
"@typescript-eslint/eslint-plugin": "8.39.1",
"@typescript-eslint/parser": "8.39.1",
"@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.5",
"msw": "2.10.4",
"npm-run-all": "4.1.5",
"path-browserify": "1.0.1",
"postcss-scss": "4.0.9",

View file

@ -16,15 +16,6 @@ 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
@ -33,6 +24,7 @@ jobs:
- centos-stream-10
- centos-stream-10-aarch64
- fedora-all
- fedora-all-aarch64
- job: copr_build
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';
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
@ -8,13 +8,6 @@ import { getHostArch, getHostDistroName, 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();
};

View file

@ -92,12 +92,7 @@ test.describe.serial('test', () => {
await frame.getByRole('button', { name: 'Create blueprint' }).click();
await expect(
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,
),
frame.locator('.pf-v6-c-card__title-text').getByText(blueprintName),
).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
set -euo pipefail
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
# 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
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"
function upload_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
mkdir -p /tmp/artifacts/extra-screenshots
USER="$(whoami)"
sudo chown -R "$USER:$USER" playwright-report
mv playwright-report /tmp/artifacts/
}
trap upload_artifacts EXIT
@ -76,12 +73,11 @@ 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 \

View file

@ -1 +1 @@
cf0a810fd3b75fa27139746c4dfe72222e13dcba
7b4735d287dd0950e0a6f47dde65b62b0f239da1

View file

@ -8,6 +8,7 @@ import {
CardHeader,
CardTitle,
Spinner,
Truncate,
} from '@patternfly/react-core';
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
@ -50,21 +51,11 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
onChange: () => dispatch(setBlueprintId(blueprint.id)),
}}
>
<CardTitle aria-label={blueprint.name}>
<CardTitle>
{isLoading && blueprint.id === selectedBlueprintId && (
<Spinner size='md' />
)}
{
// 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
}
<Truncate content={blueprint.name} position='end' />
</CardTitle>
</CardHeader>
<CardBody>{blueprint.description}</CardBody>

View file

@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import {
Bullseye,
@ -17,6 +17,7 @@ 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';
@ -28,7 +29,6 @@ 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,6 +73,16 @@ 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;
}

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Button,
@ -16,13 +16,11 @@ 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,
useGetUser,
} from '../../Hooks';
import { useComposeBPWithNotification as useComposeBlueprintMutation } from '../../Hooks';
import { useGetBlueprintQuery } from '../../store/backendApi';
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
import { useAppSelector } from '../../store/hooks';
@ -39,7 +37,18 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
useComposeBlueprintMutation();
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 () => {
if (selectedBlueprintId) {

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
Button,
@ -9,16 +9,14 @@ 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,
useGetUser,
} from '../../Hooks';
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
import {
selectBlueprintSearchInput,
@ -44,7 +42,17 @@ export const DeleteBlueprintModal: React.FunctionComponent<
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
const dispatch = useAppDispatch();
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 = {
limit: blueprintsLimit,

View file

@ -50,7 +50,6 @@ import {
Thead,
Tr,
} from '@patternfly/react-table';
import { orderBy } from 'lodash';
import { useDispatch } from 'react-redux';
import CustomHelperText from './components/CustomHelperText';
@ -67,6 +66,7 @@ import {
} from '../../../../constants';
import { useGetArchitecturesQuery } from '../../../../store/backendApi';
import {
ApiPackageSourcesResponse,
ApiRepositoryResponseRead,
ApiSearchRpmResponse,
useCreateRepositoryMutation,
@ -700,7 +700,7 @@ const Packages = () => {
);
}
let unpackedData: IBPackageWithRepositoryInfo[] =
const unpackedData: IBPackageWithRepositoryInfo[] =
combinedPackageData.flatMap((item) => {
// Spread modules into separate rows by application stream
if (item.sources) {
@ -724,16 +724,13 @@ const Packages = () => {
});
// group by name, but sort by application stream in descending order
unpackedData = orderBy(
unpackedData,
[
'name',
(pkg) => pkg.stream || '',
(pkg) => pkg.repository || '',
(pkg) => pkg.module_name || '',
],
['asc', 'desc', 'asc', 'asc'],
);
unpackedData.sort((a, b) => {
if (a.name === b.name) {
return (b.stream ?? '').localeCompare(a.stream ?? '');
} else {
return a.name.localeCompare(b.name);
}
});
if (toggleSelected === 'toggle-available') {
if (activeTabKey === Repos.INCLUDED) {
@ -869,6 +866,8 @@ const Packages = () => {
dispatch(addPackage(pkg));
if (pkg.type === 'module') {
setActiveStream(pkg.stream || '');
setActiveSortIndex(2);
setPage(1);
dispatch(
addModule({
name: pkg.module_name || '',
@ -994,18 +993,7 @@ const Packages = () => {
}
};
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 initialExpandedPkgs: IBPackageWithRepositoryInfo[] = [];
const [expandedPkgs, setExpandedPkgs] = useState(initialExpandedPkgs);
const setPkgExpanded = (
@ -1013,13 +1001,12 @@ const Packages = () => {
isExpanding: boolean,
) =>
setExpandedPkgs((prevExpanded) => {
const pkgKey = getPackageUniqueKey(pkg);
const otherExpandedPkgs = prevExpanded.filter((key) => key !== pkgKey);
return isExpanding ? [...otherExpandedPkgs, pkgKey] : otherExpandedPkgs;
const otherExpandedPkgs = prevExpanded.filter((p) => p.name !== pkg.name);
return isExpanding ? [...otherExpandedPkgs, pkg] : otherExpandedPkgs;
});
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
expandedPkgs.includes(getPackageUniqueKey(pkg));
expandedPkgs.includes(pkg);
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
@ -1043,37 +1030,51 @@ const Packages = () => {
'asc' | 'desc'
>('asc');
const sortedPackages = useMemo(() => {
if (!transformedPackages || !Array.isArray(transformedPackages)) {
return [];
}
const getSortableRowValues = (
pkg: IBPackageWithRepositoryInfo,
): (string | number | ApiPackageSourcesResponse[] | undefined)[] => {
return [pkg.name, pkg.summary, pkg.stream, pkg.end_date, pkg.repository];
};
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]);
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);
}
});
const getSortParams = (columnIndex: number) => ({
sortBy: {
@ -1099,14 +1100,14 @@ const Packages = () => {
(module) => module.name === pkg.name,
);
isSelected =
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
!isModuleWithSameName;
packages.some((p) => p.name === pkg.name) && !isModuleWithSameName;
}
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 =
packages.some((p) => p.name === pkg.name && p.stream === pkg.stream) &&
packages.some((p) => p.name === pkg.name) &&
modules.some(
(m) => m.name === pkg.module_name && m.stream === pkg.stream,
);
@ -1207,7 +1208,7 @@ const Packages = () => {
.slice(computeStart(), computeEnd())
.map((grp, rowIndex) => (
<Tbody
key={`${grp.name}-${grp.repository || 'default'}`}
key={`${grp.name}-${rowIndex}`}
isExpanded={isGroupExpanded(grp.name)}
>
<Tr data-testid='package-row'>
@ -1307,7 +1308,7 @@ const Packages = () => {
.slice(computeStart(), computeEnd())
.map((pkg, rowIndex) => (
<Tbody
key={`${pkg.name}-${pkg.stream || 'default'}-${pkg.module_name || pkg.name}`}
key={`${pkg.name}-${rowIndex}`}
isExpanded={isPkgExpanded(pkg)}
>
<Tr data-testid='package-row'>

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
ClipboardCopy,
@ -16,7 +16,6 @@ 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,
@ -25,7 +24,18 @@ import {
const RegistrationStep = () => {
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 registrationType = useAppSelector(selectRegistrationType);

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, 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,8 +44,19 @@ export const CreateSaveAndBuildBtn = ({
setIsOpen,
isDisabled,
}: CreateDropdownProps) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
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);
@ -102,7 +113,17 @@ export const CreateSaveButton = ({
isDisabled,
}: CreateDropdownProps) => {
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);

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } 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,8 +37,19 @@ export const EditSaveAndBuildBtn = ({
blueprintId,
isDisabled,
}: EditDropdownProps) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
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 packages = useAppSelector(selectPackages);
@ -94,8 +105,19 @@ export const EditSaveButton = ({
blueprintId,
isDisabled,
}: EditDropdownProps) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
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);

View file

@ -18,7 +18,6 @@ import { EditSaveAndBuildBtn, EditSaveButton } from './EditDropdown';
import {
useCreateBPWithNotification as useCreateBlueprintMutation,
useGetUser,
useUpdateBPWithNotification as useUpdateBlueprintMutation,
} from '../../../../../Hooks';
import { resolveRelPath } from '../../../../../Utilities/path';
@ -34,7 +33,6 @@ 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();
@ -54,12 +52,14 @@ 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 is fine for on prem because we save the org id
// to state through a form field in the registration step
// NOTE: This should be fine on-prem, we should
// be able to ignore the `org-id`
return mapRequestFromState(store, '');
};

View file

@ -211,9 +211,8 @@ function commonRequestToState(
snapshot_date = '';
}
// we need to check for the region for on-prem
const awsUploadOptions = aws?.upload_request
.options as AwsUploadRequestOptions & { region?: string | undefined };
.options as AwsUploadRequestOptions;
const gcpUploadOptions = gcp?.upload_request
.options as GcpUploadRequestOptions;
const azureUploadOptions = azure?.upload_request
@ -316,7 +315,6 @@ 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,

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } 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,9 +134,19 @@ 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();
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)) {
throw TypeError(

View file

@ -25,7 +25,7 @@ import {
Tr,
} from '@patternfly/react-table';
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 { NavigateFunction, useNavigate } from 'react-router-dom';
@ -58,7 +58,6 @@ import {
SEARCH_INPUT,
STATUS_POLLING_INTERVAL,
} from '../../constants';
import { useGetUser } from '../../Hooks';
import {
useGetBlueprintComposesQuery,
useGetBlueprintsQuery,
@ -88,12 +87,11 @@ 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 =
@ -106,7 +104,16 @@ const ImagesTable = () => {
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
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 = {
limit: blueprintsLimit,
@ -375,14 +382,8 @@ type AzureRowPropTypes = {
};
const AzureRow = ({ compose, rowIndex }: AzureRowPropTypes) => {
const launchEofFlag = useFlag('image-builder.launcheof');
const details = <AzureDetails compose={compose} />;
const instance = launchEofFlag ? (
<AzureLaunchModal compose={compose} />
) : (
<CloudInstance compose={compose} />
);
const instance = <CloudInstance compose={compose} />;
const status = <CloudStatus compose={compose} />;
return (
@ -402,18 +403,13 @@ 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 = launchEofFlag ? (
<OciLaunchModal compose={compose} isExpired={isExpired} />
) : (
<OciInstance compose={compose} isExpired={isExpired} />
);
const instance = <OciInstance compose={compose} isExpired={isExpired} />;
const status = (
<ExpiringStatus
compose={compose}
@ -471,8 +467,18 @@ type AwsRowPropTypes = {
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
const navigate = useNavigate();
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
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 status = <CloudStatus compose={compose} />;
@ -547,8 +553,18 @@ const Row = ({
details,
instance,
}: RowPropTypes) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
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 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';
@ -20,6 +20,7 @@ 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';
@ -30,7 +31,6 @@ import {
MODAL_ANCHOR,
SEARCH_INPUT,
} from '../../constants';
import { useGetUser } from '../../Hooks';
import {
useGetBlueprintsQuery,
useGetComposeStatusQuery,
@ -101,9 +101,19 @@ const ProvisioningLink = ({
composeStatus,
}: ProvisioningLinkPropTypes) => {
const launchEofFlag = useFlag('image-builder.launcheof');
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
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 [exposedScalprumModule, error] = useLoadModule(

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } 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,8 +122,18 @@ export const CloudStatus = ({ compose }: CloudStatusPropTypes) => {
const { data, isSuccess } = useGetComposeStatusQuery({
composeId: compose.id,
});
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
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) {
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
variant='typeahead'
onClick={handleToggle}
ref={toggleRef}
innerRef={toggleRef}
isExpanded={isOpen}
>
<TextInputGroup isPlain>

View file

@ -4,4 +4,3 @@ export { useDeleteBPWithNotification } from './MutationNotifications/useDeleteBP
export { useFixupBPWithNotification } from './MutationNotifications/useFixupBPWithNotification';
export { useComposeBPWithNotification } from './MutationNotifications/useComposeBPWithNotification';
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.
*/
const PACKAGE_VERSION = '2.10.5'
const PACKAGE_VERSION = '2.10.4'
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View file

@ -513,123 +513,6 @@ 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');
});
});
});

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') {
return [
{