diff --git a/.github/workflows/playwright-actions.yml b/.github/workflows/playwright-actions.yml index 342823675..674aebd8e 100644 --- a/.github/workflows/playwright-actions.yml +++ b/.github/workflows/playwright-actions.yml @@ -196,7 +196,7 @@ jobs: - name: Run testing proxy run: docker run -d --network=host -e HTTPS_PROXY=$RH_PROXY_URL -e ROUTES_JSON_PATH=/config/routes-ci.json -v "$(pwd)/config:/config:ro,Z" --name frontend-development-proxy quay.io/redhat-user-workloads/hcc-platex-services-tenant/frontend-development-proxy:latest - - name: Setup the minimal needed repositories (Small RHEL, EPEL 10) + - name: Setup the initial needed repositories (Hardcoded RHEL, EPEL 10, SMALL) working-directory: content-sources-backend run: make repos-minimal diff --git a/README.md b/README.md index 1d4f58044..fceae4471 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,12 @@ One can also: `yarn test` to run the unit tests directly. ## Testing with Playwright +There are 2 kinds of Playwright tests - UI tests and Itegration tests. + +### First time running Playwright + +Before running any of the Playwright tests for the first time: + 1. Ensure the correct node version is installed and in use: `nvm use` 2. Copy the [example env file](playwright_example.env) and create a file named:`.env` @@ -97,24 +103,47 @@ One can also: `yarn test` to run the unit tests directly. `yarn playwright install --with-deps` -4. Run the backend locally, steps to do this can be found in the [backend repository](https://github.com/content-services/content-sources-backend). +### Running UI Playwright tests: + +1. Run the backend locally, steps to do this can be found in the [backend repository](https://github.com/content-services/content-sources-backend). Ensure that the backend is running prior to the following steps. -5. `yarn local` will start up the front-end repository. If you do `yarn start` and choose stage, your tests will attempt to run against the stage ENV, please do not test in stage. +2. make sure values in your `.env` file are: + 1. `PROXY = ""` + 2. `INTEGRATION=""` -6. `yarn playwright test` will run the playwright test suite. `yarn playwright test --headed` will run the suite in a vnc-like browser so you can watch it's interactions. +3. run `make repo-minimal` in the backend repository to import and snapshot the initial required RedHat repositories. Wait until all the repositories become `Valid`. -For tips and recommendations on how to write Playwright tests. Check out the Playwright [style guide](/_playwright-tests/style_guide.md) in this repo. +4. `yarn local` will start up the front-end repository. If you do `yarn start` and choose stage, your tests will attempt to run against the stage ENV, please do not test in stage. -It is recommended to test using vs-code and the [Playwright Test module for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). But other editors do have similar plugins to for ease of use, if so desired. +5. `yarn playwright test` will run the playwright test suite. `yarn playwright test --headed` will run the suite in a vnc-like browser so you can watch it's interactions. + +### Running Integration Playwright tests with your frontend changes: -For running the integration tests you will need to point playwright to stage directly (i.e.: set proxy and change URL, check `playwright_example.env`), set the `INTEGRATION` flag to true and run the tests. +1. Point Playwright to stage directly in your `.env` file (check `playwright_example.env`): + 1. set proxy: `http://squid.corp.redhat.com:3128` + 2. change URL to: `https://stage.foo.redhat.com` to run against stage backend +2. set the `INTEGRATION` flag to `true` in your `.env` + +3. For Podman, uncomment the DOCKER_SOCKET option in the `.env` file, so testing containers can reach out: `DOCKER_SOCKET="/tmp/podman.sock"` + +4. For Podman to serve the API for client testing, enter the following into a terminal and let it run: `podman system service -t 0 unix:///tmp/podman.sock` + +5. `yarn start:stage` to run your local frontend against stage + +6. Run the tests For running RBAC tests locally you just need to set the RBAC environment variable to `true`. See the `playwright_example.env` file for the `RBAC` flag. +For any other `.env` variables you might need consult our `aws account`. + To add new users, edit the `ALL_USERS` array in `_playwright-tests/auth.setup.ts`. To authenticate only specific users, set `AUTH_USERS=admin,readonly` (comma-separated keys). +For tips and recommendations on how to write Playwright tests. Check out the Playwright [style guide](/_playwright-tests/style_guide.md) in this repo. + +It is recommended to test using vs-code and the [Playwright Test module for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). But other editors do have similar plugins to for ease of use, if so desired. + ### Shared Playwright test utilities This repo contains a `_playwright-tests/test-utils` git submodule which has a set of shared helpers and fixtures (and API client) across our Playwright testing suites. @@ -137,23 +166,7 @@ _I am using the regular submodule setup. When working on new tests I thought of -## Running integration tests - -## Podman - -For podman to serve the API for client testing, enter: - -``` -podman system service -t 0 unix:///tmp/podman.sock -``` - -Uncomment the DOCKER_SOCKET option in the `.env file: - -``` -DOCKER_SOCKET="/tmp/podman.sock" -``` - -## PR checks and linking front-end/back-end PRs for testing +### PR checks and linking front-end/back-end PRs for testing The CICD pipeline for playwright (both front-end and back-end) will check in the description of the front-end PRs for the following formatted text: `#testwith https://github.com/content-services/content-sources-backend/pull/` diff --git a/_playwright-tests/Integration/AssignTemplateToSystemUI.spec.ts b/_playwright-tests/Integration/AssignTemplateToSystemUI.spec.ts index f1162e2d7..8644e6449 100644 --- a/_playwright-tests/Integration/AssignTemplateToSystemUI.spec.ts +++ b/_playwright-tests/Integration/AssignTemplateToSystemUI.spec.ts @@ -74,8 +74,8 @@ test.describe('Assign Template to System via UI', () => { await nextButton.click(); await expect(page.getByText('Enter template details')).toBeVisible(); - await page.getByPlaceholder('Enter name').fill(templateName); - await page.getByPlaceholder('Description').fill('Test template for system assignment'); + await page.getByPlaceholder('Enter title').fill(templateName); + await page.getByPlaceholder('Enter detail').fill('Test template for system assignment'); await nextButton.click(); await page.getByRole('button', { name: 'Create template and add to systems' }).click(); diff --git a/_playwright-tests/Integration/AssociatedTemplateCRUD.spec.ts b/_playwright-tests/Integration/AssociatedTemplateCRUD.spec.ts index e1457774f..2afacd92b 100644 --- a/_playwright-tests/Integration/AssociatedTemplateCRUD.spec.ts +++ b/_playwright-tests/Integration/AssociatedTemplateCRUD.spec.ts @@ -62,8 +62,8 @@ test.describe('Associated Template CRUD', () => { page.getByText('Enter template details'), 'should be on the Enter template details tab', ).toBeVisible(); - await page.getByPlaceholder('Enter name').fill(`${templateName}`); - await page.getByPlaceholder('Description').fill('Template test for associated system CRUD'); + await page.getByPlaceholder('Enter title').fill(`${templateName}`); + await page.getByPlaceholder('Enter detail').fill('Template test for associated system CRUD'); await page.getByRole('button', { name: 'Next', exact: true }).click(); await page.getByRole('button', { name: 'Create other options' }).click(); diff --git a/_playwright-tests/Integration/CanUpdateSystemWithTemplate.spec.ts b/_playwright-tests/Integration/CanUpdateSystemWithTemplate.spec.ts index 34303cf02..a827bf751 100644 --- a/_playwright-tests/Integration/CanUpdateSystemWithTemplate.spec.ts +++ b/_playwright-tests/Integration/CanUpdateSystemWithTemplate.spec.ts @@ -66,8 +66,8 @@ test.describe('Test System With Template', () => { await page.getByPlaceholder('YYYY-MM-DD', { exact: true }).fill('2021-05-17'); // Older than any snapshot date await page.getByRole('button', { name: 'Next', exact: true }).click(); await expect(page.getByText('Enter template details')).toBeVisible(); - await page.getByPlaceholder('Enter name').fill(`${templateName}`); - await page.getByPlaceholder('Description').fill('Template test'); + await page.getByPlaceholder('Enter title').fill(`${templateName}`); + await page.getByPlaceholder('Enter detail').fill('Template test'); await page.getByRole('button', { name: 'Next', exact: true }).click(); await page.getByRole('button', { name: 'Create other options' }).click(); await page.getByText('Create template only', { exact: true }).click(); diff --git a/_playwright-tests/Integration/InstallUploadRepoContent.spec.ts b/_playwright-tests/Integration/InstallUploadRepoContent.spec.ts index e2bf391fe..62242971b 100644 --- a/_playwright-tests/Integration/InstallUploadRepoContent.spec.ts +++ b/_playwright-tests/Integration/InstallUploadRepoContent.spec.ts @@ -105,8 +105,8 @@ test.describe('Install Upload Repo Content', () => { await page.getByText('Use the latest content', { exact: true }).click(); await page.getByRole('button', { name: 'Next', exact: true }).click(); await expect(page.getByText('Enter template details')).toBeVisible(); - await page.getByPlaceholder('Enter name').fill(`${templateName}`); - await page.getByPlaceholder('Description').fill('Template test for upload repository'); + await page.getByPlaceholder('Enter title').fill(`${templateName}`); + await page.getByPlaceholder('Enter detail').fill('Template test for upload repository'); await page.getByRole('button', { name: 'Next', exact: true }).click(); await page.getByRole('button', { name: 'Create other options' }).click(); await page.getByText('Create template only', { exact: true }).click(); diff --git a/_playwright-tests/Integration/RegisterAndAssignSystemViaAPI.spec.ts b/_playwright-tests/Integration/RegisterAndAssignSystemViaAPI.spec.ts index b073888a9..492394375 100644 --- a/_playwright-tests/Integration/RegisterAndAssignSystemViaAPI.spec.ts +++ b/_playwright-tests/Integration/RegisterAndAssignSystemViaAPI.spec.ts @@ -46,8 +46,8 @@ test.describe('Register and assign template to systems via API', () => { await page.getByRole('button', { name: 'Next', exact: true }).click(); await expect(page.getByText('Enter template details')).toBeVisible(); - await page.getByPlaceholder('Enter name').fill(templateName); - await page.getByPlaceholder('Description').fill('Template for use template dialog test'); + await page.getByPlaceholder('Enter title').fill(templateName); + await page.getByPlaceholder('Enter detail').fill('Template for use template dialog test'); await page.getByRole('button', { name: 'Next', exact: true }).click(); await page.getByRole('button', { name: 'Create other options' }).click(); diff --git a/_playwright-tests/UI/RedHatRepo.spec.ts b/_playwright-tests/UI/RedHatRepo.spec.ts index 63ffcdd9d..8a77d25c7 100644 --- a/_playwright-tests/UI/RedHatRepo.spec.ts +++ b/_playwright-tests/UI/RedHatRepo.spec.ts @@ -3,7 +3,7 @@ import { navigateToRepositories, navigateToSnapshotsOfRepository } from './helpe import { closeGenericPopupsIfExist, waitForValidStatus } from './helpers/helpers'; test.describe('Red Hat Repositories', () => { - const smallRHRepo = 'Red Hat CodeReady Linux Builder for RHEL 9 ARM 64 (RPMs)'; + const appstreamRHRepoName = 'Red Hat Enterprise Linux 10 for ARM 64 - AppStream (RPMs)'; test.beforeEach(async ({ page }) => { await test.step('Navigate to repositories page', async () => { @@ -18,14 +18,14 @@ test.describe('Red Hat Repositories', () => { test('Verify snapshotting of Red Hat repositories', async ({ page }) => { await test.step('Wait for status to be "Valid"', async () => { - await waitForValidStatus(page, smallRHRepo, 210_000); + await waitForValidStatus(page, appstreamRHRepoName, 210_000); }); await test.step('Check repository snapshots', async () => { - const row = page.getByRole('row').filter({ hasText: smallRHRepo }); + const row = page.getByRole('row').filter({ hasText: appstreamRHRepoName }); await navigateToSnapshotsOfRepository(page, row); await expect( - page.getByTestId('snapshot_list_modal').filter({ hasText: smallRHRepo }), + page.getByTestId('snapshot_list_modal').filter({ hasText: appstreamRHRepoName }), ).toBeVisible(); }); diff --git a/_playwright-tests/UI/SnapshotRepo.spec.ts b/_playwright-tests/UI/SnapshotRepo.spec.ts index 250d883c4..ac289f829 100644 --- a/_playwright-tests/UI/SnapshotRepo.spec.ts +++ b/_playwright-tests/UI/SnapshotRepo.spec.ts @@ -107,7 +107,6 @@ test.describe('Snapshot Repositories', () => { }); test('Snapshot deletion', async ({ page, client, cleanup }) => { - const smallRHRepo = 'Red Hat CodeReady Linux Builder for RHEL 9 ARM 64 (RPMs)'; const repoNamePrefix = 'snapshot-deletion'; const repoName = `${repoNamePrefix}-${randomName()}`; const templateName = `Test-template-for-snapshot-deletion-${randomName()}`; @@ -161,33 +160,39 @@ test.describe('Snapshot Repositories', () => { await test.step('Create a template', async () => { await navigateToTemplates(page); + + // step 1 - select repo version combination await page.getByRole('button', { name: 'Create template' }).click(); await page.getByRole('button', { name: 'filter architecture' }).click(); await page.getByRole('menuitem', { name: 'aarch64' }).click(); await page.getByRole('button', { name: 'filter OS version' }).click(); - await page.getByRole('menuitem', { name: 'el9' }).click(); + await page.getByRole('menuitem', { name: 'el10' }).click(); await page.getByRole('button', { name: 'Next', exact: true }).click(); + + // step 2 - select additional repos - the hardcoded ones are already selected + // all other repositories are optional to select const modalPage = page.getByTestId('add_template_modal'); - const rowRHELRepo = await getRowByNameOrUrl(modalPage, smallRHRepo); - await rowRHELRepo.getByLabel('Select row').click(); - // wait till next button is enabled await page.getByRole('button', { name: 'Next', exact: true }).isEnabled(); await page.getByRole('button', { name: 'Next', exact: true }).click(); + // step 3 - select other repos await expect(page.getByTestId('custom_repositories_step')).toBeVisible(); const customRepo = await getRowByNameOrUrl(modalPage, repoName); await customRepo.getByLabel('Select row').click(); await page.getByRole('button', { name: 'Next', exact: true }).click(); + // step 4 - select snapshots await expect(page.getByTestId('set_up_date')).toBeVisible(); await page.getByTestId('use-latest-snapshot-radio').click(); await page.getByRole('radio', { name: 'Use the latest content' }).check(); await page.getByRole('button', { name: 'Next' }).click(); - await page.getByPlaceholder('Enter name').fill(`${templateName}`); - await page.getByPlaceholder('Description').fill('Template test'); + // step 5 - fill in template description + await page.getByPlaceholder('Enter title').fill(`${templateName}`); + await page.getByPlaceholder('Enter detail').fill('Template test'); await page.getByRole('button', { name: 'Next', exact: true }).click(); + // step 6 - create template await page.getByRole('button', { name: 'Create other options' }).click(); await page.getByText('Create template only', { exact: true }).click(); diff --git a/_playwright-tests/UI/TemplateCRUD.spec.ts b/_playwright-tests/UI/TemplateCRUD.spec.ts index d391d2441..4d29d644c 100644 --- a/_playwright-tests/UI/TemplateCRUD.spec.ts +++ b/_playwright-tests/UI/TemplateCRUD.spec.ts @@ -17,7 +17,8 @@ const repoNameX86 = `${repoNamePrefix}-x86_64-${randomName()}`; const repoNameNoSnaps = `${repoNamePrefix}-introspect-only-${randomName()}`; const templateName = `${templateNamePrefix}-${randomName()}`; -const smallRHRepo = 'Red Hat CodeReady Linux Builder for RHEL 9 ARM 64 (RPMs)'; +const appstreamRepoName = 'Red Hat Enterprise Linux 10 for ARM 64 - AppStream (RPMs)'; +const baseOSRepoName = 'Red Hat Enterprise Linux 10 for ARM 64 - BaseOS (RPMs)'; test.describe('Templates CRUD', () => { test('Add, Read, update, delete a template', async ({ page, client, cleanup, unusedRepoUrl }) => { @@ -37,7 +38,7 @@ test.describe('Templates CRUD', () => { // Create second repo (x86_64, with snapshot) const repoDataX86 = { distribution_arch: 'x86_64', - distribution_versions: ['8', '9'], + distribution_versions: ['9', '10'], name: repoNameX86, origin: 'external', snapshot: true, @@ -52,7 +53,7 @@ test.describe('Templates CRUD', () => { // Create third repo ( Any arch, introspect only, no snapshots ) const repoDataNoSnaps = { distribution_arch: '', - distribution_versions: ['8', '9'], + distribution_versions: ['9', '10'], name: repoNameNoSnaps, origin: 'external', snapshot: false, @@ -68,26 +69,49 @@ test.describe('Templates CRUD', () => { await test.step('Create a template', async () => { await navigateToTemplates(page); await page.getByRole('button', { name: 'Create template' }).click(); + + // step 1 + // Select Repo Version await page.getByRole('button', { name: 'filter architecture' }).click(); await page.getByRole('menuitem', { name: 'aarch64' }).click(); await page.getByRole('button', { name: 'filter OS version' }).click(); - await page.getByRole('menuitem', { name: 'el9' }).click(); + await page.getByRole('menuitem', { name: 'el10' }).click(); await page.getByRole('button', { name: 'Next', exact: true }).click(); + + // step 2 + // Select Additional Repos + // No additional repos required to select + const modalPage = page.getByTestId('add_template_modal'); - const rowRHELRepo = await getRowByNameOrUrl(modalPage, smallRHRepo); - await rowRHELRepo.getByLabel('Select row').click(); + + // appstream repo preselected, its checkbox disabled + const appstreamRepo = await getRowByNameOrUrl(modalPage, appstreamRepoName); + const appstreamRepoCheckbox = appstreamRepo.getByLabel('Select row'); + await expect(appstreamRepoCheckbox).toBeDisabled(); + await expect(appstreamRepoCheckbox).toBeChecked(); + + // baseOS repo preselected, its checkbox disabled + const baseOSRepo = await getRowByNameOrUrl(modalPage, baseOSRepoName); + const baseOSRepoCheckbox = baseOSRepo.getByLabel('Select row'); + await expect(baseOSRepoCheckbox).toBeDisabled(); + await expect(baseOSRepoCheckbox).toBeChecked(); + await page.getByRole('button', { name: 'Next', exact: true }).click(); + + // step 3 + // Select custom repo // Select first custom repo (aarch64) - const rowRepo = await getRowByNameOrUrl(modalPage, repoName); - await rowRepo.getByLabel('Select row').click(); + const customRepo = await getRowByNameOrUrl(modalPage, repoName); + await customRepo.getByLabel('Select row').click(); // Verify repo without snapshots appears but checkbox is disabled - const rowNoSnaps = await getRowByNameOrUrl(modalPage, repoNameNoSnaps); - await expect(rowNoSnaps).toBeVisible(); - const noSnapsCheckbox = rowNoSnaps.getByLabel('Select row'); - await expect(noSnapsCheckbox).toBeDisabled(); + const noSnapsRepo = await getRowByNameOrUrl(modalPage, repoNameNoSnaps); + await expect(noSnapsRepo).toBeVisible(); + const noSnapsRepoCheckbox = noSnapsRepo.getByLabel('Select row'); + await expect(noSnapsRepoCheckbox).toBeDisabled(); + // Verify warning message appears on hover - await noSnapsCheckbox.hover(); + await noSnapsRepoCheckbox.hover(); await expect(page.getByText('Snapshot not yet available for this repository')).toBeVisible(); // Verify x86 repo cannot be added due to architecture mismatch (should not appear) @@ -100,13 +124,23 @@ test.describe('Templates CRUD', () => { await expect(modalPage.getByText(repoNameX86)).toBeHidden(); await page.getByRole('button', { name: 'Next', exact: true }).click(); + + // step 4 + // Select snapshot await page.getByText('Use the latest content', { exact: true }).click(); await page.getByRole('button', { name: 'Next', exact: true }).click(); + + // step 5 + // Fill in template description await expect(page.getByText('Enter template details')).toBeVisible(); - await page.getByPlaceholder('Enter name').fill(`${templateName}`); - await page.getByPlaceholder('Enter name').press('Enter'); - await page.getByPlaceholder('Description').fill('Template test'); + await page.getByPlaceholder('Enter title').fill(`${templateName}`); + await page.getByPlaceholder('Enter title').press('Enter'); + await page.getByPlaceholder('Enter detail').fill('Template test'); + await page.getByPlaceholder('Enter detail').press('Enter'); await page.getByRole('button', { name: 'Next', exact: true }).click(); + + // step 6 + // Template review and template creation await page.getByRole('button', { name: 'Create other options' }).click(); await page.getByText('Create template only', { exact: true }).click(); await waitForValidStatus(page, templateName); @@ -121,7 +155,7 @@ test.describe('Templates CRUD', () => { await page.getByRole('button', { name: 'Actions' }).click(); await page.getByRole('menuitem', { name: 'Edit' }).click(); await expect( - page.getByRole('heading', { name: 'Define template content', exact: true }), + page.getByRole('heading', { name: 'Define Template Content', exact: true }), ).toBeVisible(); await page.getByRole('button', { name: 'Next', exact: true }).click(); await expect( @@ -143,10 +177,10 @@ test.describe('Templates CRUD', () => { await expect(page.getByRole('heading', { name: 'Set up date', exact: true })).toBeVisible(); await page.getByRole('button', { name: 'Next', exact: true }).click(); await expect(page.getByText('Enter template details')).toBeVisible(); - await expect(page.getByPlaceholder('Enter name')).toHaveValue(`${templateName}`); - await expect(page.getByPlaceholder('Description')).toHaveValue('Template test'); - await page.getByPlaceholder('Enter name').fill(`${templateName}-edited`); - await page.getByPlaceholder('Description').fill('Template test edited'); + await expect(page.getByPlaceholder('Enter title')).toHaveValue(`${templateName}`); + await expect(page.getByPlaceholder('Enter detail')).toHaveValue('Template test'); + await page.getByPlaceholder('Enter title').fill(`${templateName}-edited`); + await page.getByPlaceholder('Enter detail').fill('Template test edited'); await page.getByRole('button', { name: 'Next', exact: true }).click(); await page.getByRole('button', { name: 'Confirm changes', exact: true }).click(); }); diff --git a/_playwright-tests/UI/helpers/createRepositories.ts b/_playwright-tests/UI/helpers/createRepositories.ts index 6c39fb1cb..bb1aa041e 100644 --- a/_playwright-tests/UI/helpers/createRepositories.ts +++ b/_playwright-tests/UI/helpers/createRepositories.ts @@ -32,7 +32,7 @@ export const createCustomRepo = async ( const url = await unusedRepoUrl(); const repoData = { distribution_arch: 'aarch64', - distribution_versions: ['8', '9'], + distribution_versions: ['9', '10'], name: repoName, origin: 'external', snapshot: true, diff --git a/jest.config.js b/jest.config.js index bc84b09f5..e2471654e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,6 +21,7 @@ module.exports = { '^uuid$': require.resolve('uuid'), // use cjs versions of patternfly packages, not esm '^(@patternfly/[a-zA-Z0-9_-]+)/dist/esm/(.*)$': '$1/dist/js/$2', + '^@src/(.*)$': '/src/$1', }, transformIgnorePatterns: [ diff --git a/src/Pages/Repositories/ContentListTable/ContentListTable.tsx b/src/Pages/Repositories/ContentListTable/ContentListTable.tsx index b8ec14433..ebc51325c 100644 --- a/src/Pages/Repositories/ContentListTable/ContentListTable.tsx +++ b/src/Pages/Repositories/ContentListTable/ContentListTable.tsx @@ -46,14 +46,14 @@ import { } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect'; import flex from '@patternfly/react-styles/css/utilities/Flex/flex'; import { useQueryClient } from 'react-query'; -import StatusIcon from './components/StatusIcon'; +import StatusIcon from '../../../components/StatusIcon/StatusIcon'; import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; import { SkeletonTableBody } from '@patternfly/react-component-groups'; import UploadRepositoryLabel from 'components/RepositoryLabels/UploadRepositoryLabel'; import UrlWithExternalIcon from '../../../components/UrlWithLinkIcon/UrlWithLinkIcon'; import ChangedArrows from './components/SnapshotListModal/components/ChangedArrows'; import { createUseStyles } from 'react-jss'; -import PackageCount from './components/PackageCount'; +import PackageCount from '../../../components/PackageCount/PackageCount'; import DeleteKebab from '../../../components/DeleteKebab/DeleteKebab'; import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters'; import { useContentListFilters, FilterLabelsMap } from './hooks/useContentListFilters'; diff --git a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AddSystemModal.tsx b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AddSystemModal.tsx index 6941c6e03..9c472527f 100644 --- a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AddSystemModal.tsx +++ b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AddSystemModal.tsx @@ -45,6 +45,7 @@ import SystemListTable from './SystemListTable'; import ConditionalTooltip from 'components/ConditionalTooltip/ConditionalTooltip'; import useNotification from 'Hooks/useNotification'; import TagsFilter from 'components/TagsFilter/TagsFilter'; +import { isMinorRelease } from 'Pages/Templates/TemplateDetails/templateDetailHelpers'; const useStyles = createUseStyles({ mainContainer: { @@ -79,10 +80,6 @@ const useStyles = createUseStyles({ const perPageKey = 'templatesPerPage'; type FilterType = 'Name' | 'Tags'; -export const isMinorRelease = (rhsm: string) => - // Empty string means that the RHEL release version is unset and should be treated as a major release - !['', '8', '8.0', '9', '9.0', '10', '10.0'].includes(rhsm); - export default function AddSystemModal() { const queryClient = useQueryClient(); const classes = useStyles(); diff --git a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AssignTemplateModal.test.tsx b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AssignTemplateModal.test.tsx index 1b7d830b4..d08e25b0f 100644 --- a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AssignTemplateModal.test.tsx +++ b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AssignTemplateModal.test.tsx @@ -12,7 +12,7 @@ import { import type { SystemItem } from 'services/Systems/SystemsApi'; import useHasRegisteredSystems from 'Hooks/useHasRegisteredSystems'; import React from 'react'; -import { TEMPLATE_SYSTEMS_UPDATE_LIMIT } from 'Pages/Templates/TemplatesTable/components/templateHelpers'; +import { TEMPLATE_SYSTEMS_UPDATE_LIMIT } from 'Pages/Templates/TemplateDetails/templateDetailHelpers'; import userEvent from '@testing-library/user-event'; const bananaUUID = 'banana-uuid'; diff --git a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AssignTemplateModal.tsx b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AssignTemplateModal.tsx index c897c7182..c17119e06 100644 --- a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AssignTemplateModal.tsx +++ b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/AssignTemplateModal.tsx @@ -30,7 +30,7 @@ import ApiView from './ApiView'; import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; import useHasRegisteredSystems from '../../../../../Hooks/useHasRegisteredSystems'; import Loader from '../../../../../components/Loader'; -import { TEMPLATE_SYSTEMS_UPDATE_LIMIT } from 'Pages/Templates/TemplatesTable/components/templateHelpers'; +import { TEMPLATE_SYSTEMS_UPDATE_LIMIT } from 'Pages/Templates/TemplateDetails/templateDetailHelpers'; export const TEMPLATE_DOCS_URL = 'https://docs.redhat.com/en/documentation/red_hat_lightspeed/1-latest/html/managing_system_content_and_patch_updates_on_rhel_systems/patching-using-content-templates_patch-service-overview#managing-content-templates_patching-using-content-templates'; diff --git a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/SystemListTable.tsx b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/SystemListTable.tsx index 2d2b00c83..64d77de64 100644 --- a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/SystemListTable.tsx +++ b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/SystemListTable.tsx @@ -22,7 +22,7 @@ import { TEMPLATES_ROUTE } from 'Routes/constants'; import TdWithTooltip from 'components/TdWithTooltip/TdWithTooltip'; import { useParams } from 'react-router-dom'; import SystemNameCell from './components/SystemNameCell'; -import { isMinorRelease } from 'Pages/Templates/TemplatesTable/components/templateHelpers'; +import { isMinorRelease } from 'Pages/Templates/TemplateDetails/templateDetailHelpers'; const useStyles = createUseStyles({ mainContainer: { diff --git a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/SystemListView.tsx b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/SystemListView.tsx index 88f5ae179..ad198f3ca 100644 --- a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/SystemListView.tsx +++ b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/SystemListView.tsx @@ -36,7 +36,7 @@ import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; import { isMinorRelease, TEMPLATE_SYSTEMS_UPDATE_LIMIT, -} from 'Pages/Templates/TemplatesTable/components/templateHelpers'; +} from 'Pages/Templates/TemplateDetails/templateDetailHelpers'; const useStyles = createUseStyles({ topContainer: { diff --git a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/components/SystemNameCell.tsx b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/components/SystemNameCell.tsx index d7837105e..cc0768609 100644 --- a/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/components/SystemNameCell.tsx +++ b/src/Pages/Templates/TemplateDetails/components/AssignTemplateModal/components/SystemNameCell.tsx @@ -14,7 +14,7 @@ import { PATCH_SYSTEMS_ROUTE } from '../../../../../../Routes/constants'; import type { SystemItem } from '../../../../../../services/Systems/SystemsApi'; import HelpPopover from '../../../../../../components/HelpPopover'; import React from 'react'; -import { isMinorRelease } from 'Pages/Templates/TemplatesTable/components/templateHelpers'; +import { isMinorRelease } from 'Pages/Templates/TemplateDetails/templateDetailHelpers'; type Props = Pick & Pick & { diff --git a/src/Pages/Templates/TemplateDetails/components/Tabs/TemplateSystemsTab.tsx b/src/Pages/Templates/TemplateDetails/components/Tabs/TemplateSystemsTab.tsx index 22665dd07..1b2afe77c 100644 --- a/src/Pages/Templates/TemplateDetails/components/Tabs/TemplateSystemsTab.tsx +++ b/src/Pages/Templates/TemplateDetails/components/Tabs/TemplateSystemsTab.tsx @@ -33,7 +33,7 @@ import useDebounce from 'Hooks/useDebounce'; import useHasRegisteredSystems from 'Hooks/useHasRegisteredSystems'; import useRootPath from 'Hooks/useRootPath'; import useSafeUUIDParam from '../../../../../Hooks/useSafeUUIDParam'; -import { TEMPLATE_SYSTEMS_UPDATE_LIMIT } from 'Pages/Templates/TemplatesTable/components/templateHelpers'; +import { TEMPLATE_SYSTEMS_UPDATE_LIMIT } from 'Pages/Templates/TemplateDetails/templateDetailHelpers'; const useStyles = createUseStyles({ description: { diff --git a/src/Pages/Templates/TemplateDetails/templateDetailHelpers.ts b/src/Pages/Templates/TemplateDetails/templateDetailHelpers.ts new file mode 100644 index 000000000..42552e80a --- /dev/null +++ b/src/Pages/Templates/TemplateDetails/templateDetailHelpers.ts @@ -0,0 +1,5 @@ +export const isMinorRelease = (rhsm: string) => + // Empty string means that the RHEL release version is unset and should be treated as a major release + !['', '8', '8.0', '9', '9.0', '10', '10.0'].includes(rhsm); + +export const TEMPLATE_SYSTEMS_UPDATE_LIMIT = 1000; diff --git a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/AddOrEditTemplate.tsx b/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/AddOrEditTemplate.tsx deleted file mode 100644 index bc3aaec03..000000000 --- a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/AddOrEditTemplate.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import { - Bullseye, - Modal, - ModalVariant, - Spinner, - Wizard, - WizardFooterWrapper, - WizardHeader, - WizardStep, - useWizardContext, -} from '@patternfly/react-core'; - -import { useNavigate, useSearchParams, useLocation } from 'react-router-dom'; -import { useCreateTemplateQuery, useEditTemplateQuery } from 'services/Templates/TemplateQueries'; -import { AddTemplateContextProvider, useAddTemplateContext } from './AddTemplateContext'; -import RedhatRepositoriesStep from './steps/RedhatRepositoriesStep'; -import CustomRepositoriesStep from './steps/CustomRepositoriesStep'; -import { TemplateRequest } from 'services/Templates/TemplateApi'; - -import DefineContentStep from './steps/DefineContentStep'; -import SetUpDateStep from './steps/SetUpDateStep'; -import DetailStep from './steps/DetailStep'; -import ReviewStep from './steps/ReviewStep'; -import { formatTemplateDate } from 'helpers'; -import { isEmpty } from 'lodash'; -import { createUseStyles } from 'react-jss'; -import { useEffect, useRef } from 'react'; -import { AddNavigateButton } from './AddNavigateButton'; -import useRootPath from 'Hooks/useRootPath'; -import { TEMPLATES_ROUTE } from 'Routes/constants'; - -const useStyles = createUseStyles({ - minHeightForSpinner: { - minHeight: '60vh', - }, -}); - -export interface Props { - isDisabled: boolean; - addRepo: (snapshot: boolean) => void; -} - -const DEFAULT_STEP_ID = 'define-content'; - -const stepIdToIndex: Record = { - 'define-content': 2, - 'redhat-repositories': 3, - 'custom-repositories': 4, - 'set-up-date': 5, - detail: 6, - review: 7, -}; - -// Component to sync URL with wizard state (must be inside Wizard) -const WizardUrlSync = ({ onCancel }: { onCancel: () => void }) => { - const { goToStepById, activeStep } = useWizardContext(); - const [urlSearchParams] = useSearchParams(); - - const tabParam = urlSearchParams.get('tab'); - - useEffect(() => { - if (tabParam && tabParam !== activeStep?.id) { - if (stepIdToIndex[tabParam]) { - goToStepById(tabParam); - } else if (tabParam === 'content') { - goToStepById(DEFAULT_STEP_ID); - } else { - onCancel(); - } - } - }, [tabParam, activeStep?.id, goToStepById, onCancel]); - - return null; -}; - -const AddOrEditTemplateBase = () => { - const classes = useStyles(); - const navigate = useNavigate(); - const location = useLocation(); - const rootPath = useRootPath(); - const [urlSearchParams, setUrlSearchParams] = useSearchParams(); - - // Store the original 'from' value on mount (before step navigation changes location.state) - const fromRef = useRef(location.state?.from); - - const { isEdit, templateRequest, checkIfCurrentStepValid, editUUID } = useAddTemplateContext(); - - // useSafeUUIDParam in AddTemplateContext already validates the UUID - // If in edit mode and UUID is invalid, it will be an empty string - if (isEdit && !editUUID) throw new Error('UUID is invalid'); - - const tabParam = urlSearchParams.get('tab'); - const initialIndex = tabParam && stepIdToIndex[tabParam] ? stepIdToIndex[tabParam] : 2; - - useEffect(() => { - if (!urlSearchParams.get('tab')) { - setUrlSearchParams({ tab: DEFAULT_STEP_ID }, { replace: true }); - } - }, []); - - const { queryClient } = useAddTemplateContext(); - - const onCancel = () => { - if (fromRef.current === 'table') { - navigate(`${rootPath}/${TEMPLATES_ROUTE}`); - } else if (isEdit && editUUID) { - navigate(`${rootPath}/${TEMPLATES_ROUTE}/${editUUID}`); - } else { - navigate(`${rootPath}/${TEMPLATES_ROUTE}`); - } - }; - - const { mutateAsync: addTemplate, isLoading: isAdding } = useCreateTemplateQuery(queryClient, { - ...(templateRequest as TemplateRequest), - date: templateRequest.use_latest ? null : formatTemplateDate(templateRequest.date || ''), - }); - - const { mutateAsync: editTemplate, isLoading: isEditing } = useEditTemplateQuery(queryClient, { - uuid: editUUID as string, - ...(templateRequest as TemplateRequest), - date: templateRequest.use_latest ? null : formatTemplateDate(templateRequest.date || ''), - }); - - const sharedFooterProps = { - nextButtonProps: { ouiaId: 'wizard-next-btn' }, - backButtonProps: { ouiaId: 'wizard-back-btn' }, - cancelButtonProps: { ouiaId: 'wizard-cancel-btn' }, - }; - - return ( - - {isEdit && isEmpty(templateRequest) ? ( - - - - ) : ( - - - - - } - onClose={onCancel} - startIndex={initialIndex} - onStepChange={(_, currentStep) => { - setUrlSearchParams({ tab: String(currentStep.id) }); - }} - > - - - , - - - , - - - , - ]} - /> - - - - {/* */} - - - - editTemplate().then(() => onCancel()), - isNextDisabled: isEditing, - } - ) : ( - - - - ) - } - > - - - - )} - - ); -}; - -// Wrap the modal with the provider -export function AddOrEditTemplate() { - return ( - - - - ); -} diff --git a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/AddTemplateContext.tsx b/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/AddTemplateContext.tsx deleted file mode 100644 index 2ec7a1781..000000000 --- a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/AddTemplateContext.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { - createContext, - ReactNode, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; -import { TemplateRequest } from 'services/Templates/TemplateApi'; -import { QueryClient, useQueryClient } from 'react-query'; -import { useContentListQuery, useRepositoryParams } from 'services/Content/ContentQueries'; -import { ContentOrigin, NameLabel } from 'services/Content/ContentApi'; -import { hardcodeRedHatReposByArchAndVersion } from '../templateHelpers'; -import { useNavigate } from 'react-router-dom'; -import { useFetchTemplate } from 'services/Templates/TemplateQueries'; -import useRootPath from 'Hooks/useRootPath'; -import { isDateValid } from 'helpers'; -import useSafeUUIDParam from 'Hooks/useSafeUUIDParam'; - -export interface AddTemplateContextInterface { - queryClient: QueryClient; - distribution_arches: NameLabel[]; - distribution_versions: NameLabel[]; - templateRequest: Partial; - setTemplateRequest: (value: React.SetStateAction>) => void; - selectedRedhatRepos: Set; - setSelectedRedhatRepos: (uuidSet: Set) => void; - selectedCustomRepos: Set; - setSelectedCustomRepos: (uuidSet: Set) => void; - hardcodedRedhatRepositoryUUIDS: Set; - checkIfCurrentStepValid: (index: number) => boolean; - isEdit?: boolean; - editUUID?: string; -} - -export const AddTemplateContext = createContext({} as AddTemplateContextInterface); - -export const AddTemplateContextProvider = ({ children }: { children: ReactNode }) => { - const uuid = useSafeUUIDParam('templateUUID'); - const { data: editTemplateData, isError } = useFetchTemplate(uuid, !!uuid); - - const navigate = useNavigate(); - const rootPath = useRootPath(); - - if (isError) navigate(rootPath); - const [templateRequest, setTemplateRequest] = useState>({}); - const [selectedRedhatRepos, setSelectedRedhatRepos] = useState>(new Set()); - const [selectedCustomRepos, setSelectedCustomRepos] = useState>(new Set()); - const [hardcodedRedhatRepositories, setHardcodeRepositories] = useState([]); - const [hardcodedRedhatRepositoryUUIDS, setHardcodeRepositoryUUIDS] = useState>( - new Set(), - ); - - const stepsValidArray = useMemo(() => { - const { arch, date, name, version, use_latest } = templateRequest; - - return [ - true, - arch && version, - !!selectedRedhatRepos.size, - true, - use_latest || isDateValid(date ?? ''), - !!name && name.length < 256, - ] as boolean[]; - }, [templateRequest, selectedRedhatRepos.size]); - - const checkIfCurrentStepValid = useCallback( - (stepIndex: number) => { - const stepsToCheck = stepsValidArray.slice(0, stepIndex + 1); - return !stepsToCheck.every((step) => step); - }, - [selectedRedhatRepos.size, stepsValidArray], - ); - - const queryClient = useQueryClient(); - - const { data } = useContentListQuery( - 1, - 10, - { urls: hardcodedRedhatRepositories }, - '', - [ContentOrigin.REDHAT], - !!hardcodedRedhatRepositories.length, - ); - - const { data: existingRepositoryInformation, isLoading } = useContentListQuery( - 1, - 10, - { uuids: editTemplateData?.repository_uuids }, - '', - [ContentOrigin.ALL], - !!uuid && !!editTemplateData?.repository_uuids.length, - ); - - useEffect(() => { - if (!!templateRequest.arch && !!templateRequest.version) { - const result = hardcodeRedHatReposByArchAndVersion( - templateRequest.arch, - templateRequest.version, - ); - if (result) { - setHardcodeRepositories(result); - } - if (!uuid) setSelectedCustomRepos(new Set()); - } - }, [templateRequest.version, templateRequest.arch, uuid]); - - useEffect(() => { - if (data?.data?.length) { - const hardcodedItems = data?.data.map((item) => item.uuid); - - setHardcodeRepositoryUUIDS(new Set(hardcodedItems)); - setSelectedRedhatRepos( - new Set( - selectedRedhatRepos.has(hardcodedItems[0]) - ? [...selectedRedhatRepos, ...hardcodedItems] - : hardcodedItems, - ), - ); - } - }, [data?.data]); - - // If editing, we want to load in the current data - useEffect(() => { - if (uuid && !!editTemplateData && !isLoading && !!existingRepositoryInformation) { - const startingState = { - ...editTemplateData, - }; - - setTemplateRequest(startingState); - const redHatReposToAdd: string[] = []; - const customReposToAdd: string[] = []; - - existingRepositoryInformation?.data.forEach((item) => { - if (item.org_id === '-1') { - redHatReposToAdd.push(item.uuid); - } else { - customReposToAdd.push(item.uuid); - } - }); - - if (redHatReposToAdd.length) { - setSelectedRedhatRepos(new Set([...selectedRedhatRepos, ...redHatReposToAdd])); - } - - if (customReposToAdd.length) { - setSelectedCustomRepos(new Set(customReposToAdd)); - } - } - }, [editTemplateData, isLoading, existingRepositoryInformation]); - - const templateRequestDependencies = useMemo( - () => [...selectedCustomRepos, ...selectedRedhatRepos], - [selectedCustomRepos, selectedRedhatRepos], - ); - - useEffect(() => { - setTemplateRequest((prev) => ({ - ...prev, - repository_uuids: [...selectedRedhatRepos, ...selectedCustomRepos], - })); - }, [templateRequestDependencies]); - - const { - data: { distribution_versions, distribution_arches } = { - distribution_versions: [], - distribution_arches: [], - }, - } = useRepositoryParams(); - - return ( - - {children} - - ); -}; - -export const useAddTemplateContext = () => useContext(AddTemplateContext); diff --git a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/steps/CustomRepositoriesStep.tsx b/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/steps/CustomRepositoriesStep.tsx deleted file mode 100644 index a7d9c4bee..000000000 --- a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/steps/CustomRepositoriesStep.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import { - Bullseye, - Button, - Flex, - FlexItem, - Grid, - InputGroup, - InputGroupItem, - Pagination, - PaginationVariant, - Content, - TextInput, - ContentVariants, - Title, - ToggleGroup, - ToggleGroupItem, -} from '@patternfly/react-core'; -import { useAddTemplateContext } from '../AddTemplateContext'; -import { createUseStyles } from 'react-jss'; -import { ContentItem, ContentOrigin } from 'services/Content/ContentApi'; -import { useState } from 'react'; -import { CONTENT_LIST_KEY, useContentListQuery } from 'services/Content/ContentQueries'; -import { ExternalLinkAltIcon, SearchIcon, SyncAltIcon } from '@patternfly/react-icons'; -import EmptyTableState from 'components/EmptyTableState/EmptyTableState'; -import { useHref } from 'react-router-dom'; -import Hide from 'components/Hide/Hide'; -import { SkeletonTable } from '@patternfly/react-component-groups'; -import { Table, TableVariant, Tbody, Td, Th, ThProps, Thead, Tr } from '@patternfly/react-table'; -import UrlWithExternalIcon from 'components/UrlWithLinkIcon/UrlWithLinkIcon'; -import PackageCount from 'Pages/Repositories/ContentListTable/components/PackageCount'; -import StatusIcon from 'Pages/Repositories/ContentListTable/components/StatusIcon'; -import useDebounce from 'Hooks/useDebounce'; -import { ADD_ROUTE, REPOSITORIES_ROUTE } from 'Routes/constants'; -import TdWithTooltip from 'components/TdWithTooltip/TdWithTooltip'; -import ConditionalTooltip from 'components/ConditionalTooltip/ConditionalTooltip'; -import { isEPELUrl, reduceStringToCharsWithEllipsis } from 'helpers'; -import UploadRepositoryLabel from 'components/RepositoryLabels/UploadRepositoryLabel'; -import CommunityRepositoryLabel from 'components/RepositoryLabels/CommunityRepositoryLabel'; -import CustomEpelWarning from 'components/RepositoryLabels/CustomEpelWarning'; -import { useAppContext } from 'middleware/AppContext'; - -const useStyles = createUseStyles({ - topBottomContainers: { - justifyContent: 'space-between', - height: 'fit-content', - }, - invisible: { - opacity: 0, - }, - reduceTrailingMargin: { - marginRight: '12px!important', - }, -}); - -export default function CustomRepositoriesStep() { - const classes = useStyles(); - const path = useHref('content'); - const pathname = path.split('content')[0] + 'content'; - - const { queryClient, templateRequest, selectedCustomRepos, setSelectedCustomRepos } = - useAddTemplateContext(); - const { features } = useAppContext(); - - const [toggled, setToggled] = useState(false); - - const setUUIDForList = (uuid: string) => { - if (selectedCustomRepos.has(uuid)) { - selectedCustomRepos.delete(uuid); - if (selectedCustomRepos.size === 0) { - setToggled(false); - } - } else { - selectedCustomRepos.add(uuid); - } - setSelectedCustomRepos(new Set(selectedCustomRepos)); - }; - - const storedPerPage = Number(20); - - const [searchQuery, setSearchQuery] = useState(''); - const debouncedSearch = useDebounce(searchQuery); - - const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(storedPerPage); - const [activeSortIndex, setActiveSortIndex] = useState(0); - const [activeSortDirection, setActiveSortDirection] = useState<'asc' | 'desc'>('asc'); - - const onSetPage = (_, newPage: number) => setPage(newPage); - const onPerPageSelect = (_, newPerPage: number, newPage: number) => { - setPerPage(newPerPage); - setPage(newPage); - }; - - const columnHeaders = ['Name', 'Status', 'Packages']; - - const columnSortAttributes = ['name', 'status', 'package_count']; - - const sortString = (): string => - columnSortAttributes[activeSortIndex] + ':' + activeSortDirection; - - const sortParams = (columnIndex: number): ThProps['sort'] => ({ - sortBy: { - index: activeSortIndex, - direction: activeSortDirection, - defaultDirection: 'asc', // starting sort direction when first sorting a column. Defaults to 'asc' - }, - onSort: (_event, index, direction) => { - setActiveSortIndex(index); - setActiveSortDirection(direction); - }, - columnIndex, - }); - - const { - isLoading, - isFetching, - data = { data: [], meta: { count: 0, limit: 20, offset: 0 } }, - } = useContentListQuery( - page, - perPage, - { - search: searchQuery === '' ? searchQuery : debouncedSearch, - availableForArch: templateRequest.arch as string, - availableForVersion: templateRequest.version as string, - uuids: toggled ? [...selectedCustomRepos] : undefined, - }, - sortString(), - [ContentOrigin.CUSTOM, ContentOrigin.COMMUNITY], - ); - - const { - data: contentList = [], - meta: { count = 0 }, - } = data; - const countIsZero = count === 0; - const showLoader = countIsZero && !isLoading; - - const isEPELRepository = (repo: ContentItem): boolean => { - if (repo.origin === ContentOrigin.COMMUNITY) { - return true; - } - - return isEPELUrl(repo.url); - }; - - const isAnyEPELRepoSelected = (): boolean => - contentList.some((repo) => selectedCustomRepos.has(repo.uuid) && isEPELRepository(repo)); - - return ( - - - - Other repositories - - - - - - Select custom or EPEL repositories. - - - - - - - - - - setSearchQuery(value)} - type='search' - customIcon={} - /> - - - - - - - setToggled(false)} - /> - setToggled(true)} - /> - - - - - - - - - - - - {showLoader ? ( - - setSearchQuery('')} - itemName='custom repositories' - notFilteredBody='To get started, create a custom repository' - notFilteredButton={ - - } - /> - - ) : ( - <> - - - - - - - - - - - ))} - - - - {contentList.map((rowData: ContentItem, rowIndex) => { - const { uuid, name, url, origin } = rowData; - const shouldDisableOtherEPEL = - isEPELRepository(rowData) && - isAnyEPELRepoSelected() && - !selectedCustomRepos.has(uuid); - - return ( - - setUUIDForList(uuid), - isSelected: selectedCustomRepos.has(uuid), - isDisabled: - !(rowData.snapshot && rowData.last_snapshot_uuid) || - shouldDisableOtherEPEL, - }} - /> - - - - - ); - })} - -
- {columnHeaders.map((columnHeader, index) => ( - - {columnHeader} -
- 60} content={name}> - <> - {reduceStringToCharsWithEllipsis(name, 60)} - - - - - - - - - - - - - 50} content={url}> - - - - - - - -
- - - - - - - - -
- - )} -
- ); -} diff --git a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/steps/DefineContentStep.tsx b/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/steps/DefineContentStep.tsx deleted file mode 100644 index d9134162e..000000000 --- a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/steps/DefineContentStep.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { - ExpandableSection, - Form, - FormGroup, - Grid, - Content, - ContentVariants, - Title, - MenuToggle, - Dropdown, - DropdownItem, - DropdownList, -} from '@patternfly/react-core'; -import { useAddTemplateContext } from '../AddTemplateContext'; -import ConditionalTooltip from 'components/ConditionalTooltip/ConditionalTooltip'; -import { useState } from 'react'; -import { createUseStyles } from 'react-jss'; - -const useStyles = createUseStyles({ - fullWidth: { - width: '100%!important', - maxWidth: 'unset!important', - }, -}); - -export default function DefineContentStep() { - const classes = useStyles(); - const [archOpen, setArchOpen] = useState(false); - const [versionOpen, setVersionOpen] = useState(false); - const { - isEdit, - templateRequest, - setTemplateRequest, - distribution_versions, - distribution_arches, - } = useAddTemplateContext(); - - const archesDisplay = (arch?: string) => - distribution_arches.find(({ label }) => arch === label)?.name || 'Select architecture'; - - const versionDisplay = (version?: string): string => - // arm64 aarch64 - distribution_versions.find(({ label }) => version === label)?.name || 'Select OS version'; - - return ( - - - Define template content - - - Templates provide consistent content across environments and time. They enable you to - control the scope of package and advisory updates that will be installed on selected - systems. - - - Preselect available content - - Based on your filters, the base repositories will be added to this template. - - -
- - { - setTemplateRequest((prev) => ({ ...prev, arch: val as string })); - setArchOpen(false); - }} - toggle={(toggleRef) => ( - - setArchOpen((prev) => !prev)} - isExpanded={archOpen} - > - {archesDisplay(templateRequest?.arch)} - - - )} - onOpenChange={(isOpen) => setArchOpen(isOpen)} - isOpen={archOpen} - > - - {distribution_arches - .filter(({ label }) => ['x86_64', 'aarch64'].includes(label)) - .map(({ label, name }) => ( - - {name} - - ))} - - - - - { - setTemplateRequest((prev) => ({ ...prev, version: val as string })); - setVersionOpen(false); - }} - toggle={(toggleRef) => ( - - setVersionOpen((prev) => !prev)} - isExpanded={versionOpen} - > - {versionDisplay(templateRequest?.version)} - - - )} - onOpenChange={(isOpen) => setVersionOpen(isOpen)} - isOpen={versionOpen} - > - - {distribution_versions - .filter(({ label }) => ['8', '9', '10'].includes(label)) - .map(({ label, name }) => ( - - {name} - - ))} - - - -
- - - - - Clients are configured to use date-based snapshots of Red Hat and custom repositories. - - Third-party tooling is used to update systems. - {/* Build Images from date based repository snapshots. */} - - - -
- ); -} diff --git a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/steps/DetailStep.tsx b/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/steps/DetailStep.tsx deleted file mode 100644 index 403819672..000000000 --- a/src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/steps/DetailStep.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { - Form, - FormGroup, - Grid, - Content, - TextArea, - TextInput, - ContentVariants, - Title, -} from '@patternfly/react-core'; -import { useAddTemplateContext } from '../AddTemplateContext'; -import { useState } from 'react'; -import { TemplateValidationSchema } from '../../templateHelpers'; -import CustomHelperText from 'components/CustomHelperText/CustomHelperText'; - -export default function DetailStep() { - const { templateRequest, setTemplateRequest } = useAddTemplateContext(); - const [errors, setErrors] = useState({ name: '', description: '' }); - - const setFieldValues = (value: string, field: 'name' | 'description') => { - setTemplateRequest((prev) => ({ ...prev, [field]: value })); - try { - TemplateValidationSchema.validateSyncAt(field, { [field]: value }); - if (errors[field]) setErrors({ ...errors, [field]: '' }); - } catch (err) { - const message = (err as Error).message; - setErrors({ ...errors, [field]: message }); - } - }; - - return ( - - - Enter template details - - - Enter a name and a description for your template. - - -
- - setFieldValues(value, 'name')} - value={templateRequest?.name || ''} - placeholder='Enter name' - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); - } - }} - /> - - - -