Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions libs/cypress/e2e/devices/approveEnrollmentRequest.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ApproveEnrollmentRequestModalPage } from '../../pages/ApproveEnrollment

let devicesPage: DevicesPage;
let approveERModalPage: ApproveEnrollmentRequestModalPage;
const FIRST_PENDING_ENROLLMENT_REQUEST_NAME = 'a021622d8633782719874da4052f957faa742fc7050026748bc79065c8819d139';
const SECOND_PAGE_PENDING_ENROLLMENT_REQUEST_NAME = '051aad6133782719874da4052f957faa74270500267873f79066f';

describe('Enrollment requests approval', () => {
beforeEach(() => {
Expand All @@ -22,10 +24,7 @@ describe('Enrollment requests approval', () => {
// Validate that the enrollment request details shown are correct
approveERModalPage = new ApproveEnrollmentRequestModalPage();
approveERModalPage.modalTitle.should('contain.text', 'Approve pending device');
approveERModalPage.deviceName.should(
'contain.text',
'a021622d8633782719874da4052f957faa742fc7050026748bc79065c8819d139',
);
approveERModalPage.deviceName.should('contain.text', FIRST_PENDING_ENROLLMENT_REQUEST_NAME);

// Define a new label. The field for adding the new label is focused and can be changed
approveERModalPage.addNewLabelButton.click();
Expand All @@ -41,4 +40,33 @@ describe('Enrollment requests approval', () => {
// NOTE: The ER will still appear in the device list as such.
// To mock it properly, we'd need to remove it from the ER list, and add its equivalent item to the Device list.
});

it('Pending enrollment request filters reset pagination', () => {
cy.wait('@all-enrollment-requests');

devicesPage.pendingEnrollmentRequestsNextPage.click();
cy.wait('@all-enrollment-requests');
devicesPage.firstEnrollmentRequestRow.should('contain.text', SECOND_PAGE_PENDING_ENROLLMENT_REQUEST_NAME);

devicesPage.enrollmentRequestSearchInput.type(FIRST_PENDING_ENROLLMENT_REQUEST_NAME);
cy.wait('@all-enrollment-requests');
devicesPage.firstEnrollmentRequestRow.should('contain.text', FIRST_PENDING_ENROLLMENT_REQUEST_NAME);
});

it('Pending enrollment requests remain visible while clearing an empty search result', () => {
cy.wait('@all-enrollment-requests');
devicesPage.firstEnrollmentRequestRow.should('be.visible');

devicesPage.enrollmentRequestSearchInput.type('fake');
cy.wait('@all-enrollment-requests');
devicesPage.pendingEnrollmentRequestsNoResults.should('be.visible');
devicesPage.firstEnrollmentRequestRow.should('not.exist');

devicesPage.enrollmentRequestSearchInput.clear();
devicesPage.pendingEnrollmentRequestsSection.should('be.visible');
Comment thread
eldar101 marked this conversation as resolved.
devicesPage.pendingEnrollmentRequestsLoading.should('be.visible');

cy.wait('@all-enrollment-requests');
devicesPage.firstEnrollmentRequestRow.scrollIntoView().should('be.visible');
});
});
6 changes: 3 additions & 3 deletions libs/cypress/fixtures/enrollmentRequests/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConditionStatus, ConditionType } from '@flightctl/types';
import { ConditionStatus, ConditionType, type EnrollmentRequest } from '@flightctl/types';
import { API_VERSION } from '../../support/constants';

const approvedErStatus = {
Expand All @@ -20,7 +20,7 @@ const approvedErStatus = {
],
};

const getErList = (onlyPending: boolean) =>
const getErList = (onlyPending: boolean): EnrollmentRequest[] =>
[
{
apiVersion: API_VERSION,
Expand Down Expand Up @@ -75,7 +75,7 @@ const getErList = (onlyPending: boolean) =>
status: { conditions: [] },
},
].filter((er) => {
return onlyPending ? er.status.conditions.length === 0 : true;
return onlyPending ? er.status?.conditions?.length === 0 : true;
});

export { getErList };
24 changes: 24 additions & 0 deletions libs/cypress/pages/DevicesPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,30 @@ export class DevicesPage {
return cy.get(`[data-testid=enrollment-request-0] button[aria-label="Kebab toggle"]`);
}

get enrollmentRequestSearchInput() {
return cy.get('[data-testid="pending-enrollment-request-search-input"]');
}

get pendingEnrollmentRequestsSection() {
return cy.get('[data-testid="pending-enrollment-requests-section"]');
}
Comment thread
eldar101 marked this conversation as resolved.

get pendingEnrollmentRequestsLoading() {
return cy.get('[data-testid="pending-enrollment-requests-loading"]');
}

get pendingEnrollmentRequestsNoResults() {
return this.pendingEnrollmentRequestsSection.contains('No results found');
}

get firstEnrollmentRequestRow() {
return cy.get('[data-testid="enrollment-request-0"]');
}

get pendingEnrollmentRequestsNextPage() {
return cy.get('button[aria-label="Go to next page"]');
}

enrollmentRequestKebabMenuAction(actionName: string) {
return cy.get('[role="menuitem"]').contains(actionName);
}
Expand Down
90 changes: 84 additions & 6 deletions libs/cypress/support/interceptors/enrollmentRequests.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
import { getErList } from '../../fixtures';
import { EnrollmentRequest } from '@flightctl/types';
import type { EnrollmentRequest, EnrollmentRequestList } from '@flightctl/types';
import { API_VERSION } from '../constants';
import { createListMatcher } from './matchers';

const buildErResponse = (enrollmentRequests: EnrollmentRequest[]) => ({
const UNFILTERED_RESPONSE_DELAY_MS = 1000;
const TEST_PAGE_SIZE = 15;
const TEST_PENDING_ENROLLMENT_COUNT = TEST_PAGE_SIZE + 1;
const FIRST_PAGE_CONTINUE_TOKEN = 'page-2';

const buildErResponse = (enrollmentRequests: EnrollmentRequest[]): EnrollmentRequestList => ({
apiVersion: API_VERSION,
items: enrollmentRequests,
kind: 'EnrollmentRequestList',
metadata: {},
});

let shouldDelayNextUnfilteredPendingEnrollmentResponse = false;

const loadInterceptors = () => {
cy.intercept('GET', createListMatcher('enrollmentrequests'), (req) => {
const hasFieldSelector = req.url.includes('fieldSelector=');
req.reply({
body: buildErResponse(getErList(hasFieldSelector)),
});
const requestUrl = new URL(req.url);
const fieldSelector = requestUrl.searchParams.get('fieldSelector') || '';
const hasFieldSelector = !!fieldSelector;
const enrollmentRequests = filterEnrollmentRequests(getTestEnrollmentRequests(hasFieldSelector), fieldSelector);
const pageStart = requestUrl.searchParams.get('continue') === FIRST_PAGE_CONTINUE_TOKEN ? TEST_PAGE_SIZE : 0;
const pageItems = enrollmentRequests.slice(pageStart, pageStart + TEST_PAGE_SIZE);
const remainingItemCount = Math.max(enrollmentRequests.length - pageStart - pageItems.length, 0);
const body = buildErResponse(pageItems);
body.metadata = {
...(remainingItemCount > 0 ? { continue: FIRST_PAGE_CONTINUE_TOKEN } : {}),
remainingItemCount,
};

if (
shouldDelayNextUnfilteredPendingEnrollmentResponse &&
isUnfilteredPendingEnrollmentRequest(requestUrl, fieldSelector)
) {
shouldDelayNextUnfilteredPendingEnrollmentResponse = false;
req.reply({
body,
delayMs: UNFILTERED_RESPONSE_DELAY_MS,
});
return;
}

if (getNameSearch(fieldSelector) && enrollmentRequests.length === 0) {
shouldDelayNextUnfilteredPendingEnrollmentResponse = true;
}

req.reply({ body });
}).as('all-enrollment-requests');

cy.intercept('PUT', '/api/flightctl/api/v1/enrollmentrequests/*/approval', (req) => {
Expand All @@ -25,4 +58,49 @@ const loadInterceptors = () => {
}).as('approve-enrollment-request');
};

const filterEnrollmentRequests = (
enrollmentRequests: EnrollmentRequest[],
fieldSelector: string,
): EnrollmentRequest[] => {
const nameSearch = getNameSearch(fieldSelector);
if (!nameSearch) {
return enrollmentRequests;
}
return enrollmentRequests.filter((er) => er.metadata.name?.includes(nameSearch));
};

const getTestEnrollmentRequests = (onlyPending: boolean): EnrollmentRequest[] => {
const enrollmentRequests = getErList(onlyPending);
if (!onlyPending || enrollmentRequests.length < 2 || enrollmentRequests.length >= TEST_PENDING_ENROLLMENT_COUNT) {
return enrollmentRequests;
}

const firstPendingEnrollment = enrollmentRequests[0];
const lastPendingEnrollment = enrollmentRequests[enrollmentRequests.length - 1];
const fillerEnrollmentCount = TEST_PENDING_ENROLLMENT_COUNT - enrollmentRequests.length;
const fillerEnrollments = Array.from({ length: fillerEnrollmentCount }, (_, index) => ({
...firstPendingEnrollment,
metadata: {
...firstPendingEnrollment.metadata,
name: `${firstPendingEnrollment.metadata.name || 'pending-enrollment'}-filler-${index}`,
},
}));

return [firstPendingEnrollment, ...fillerEnrollments, lastPendingEnrollment];
};

const getNameSearch = (fieldSelector: string): string | undefined => {
for (const selector of fieldSelector.split(',')) {
const match = selector.match(/^metadata\.name contains ([^,]+)$/);
if (match) {
return match[1];
}
}
return undefined;
};

const isUnfilteredPendingEnrollmentRequest = (requestUrl: URL, fieldSelector: string): boolean => {
return fieldSelector === '!status.approval.approved' && requestUrl.searchParams.get('limit') === '15';
};

export { loadInterceptors };
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ const EnrollmentRequestList = ({ refetchDevices, isStandalone }: EnrollmentReque
},
});

// In non-standalone mode, hide the entire component when the search result is empty (and not due to filtering)
const isLastUnfilteredListEmpty = !search && itemCount === 0;
const isInitialUnfilteredLoad = !search && itemCount === 0 && isLoading;

// In non-standalone mode, hide the entire component when the unfiltered list is empty.
const isLastUnfilteredListEmpty = !search && itemCount === 0 && !isLoading;
Comment thread
eldar101 marked this conversation as resolved.
if (!isStandalone && isLastUnfilteredListEmpty) {
return null;
}
Expand All @@ -93,8 +95,13 @@ const EnrollmentRequestList = ({ refetchDevices, isStandalone }: EnrollmentReque
title={t('Devices pending approval')}
headingLevel="h2"
description={t('Review and approve devices requesting to join your environment.')}
testId="pending-enrollment-requests-section"
>
<ListPageBody error={error} loading={false}>
<ListPageBody
error={error}
loading={!isStandalone && isInitialUnfilteredLoad}
loadingTestId="pending-enrollment-requests-loading"
>
<EnrollmentRequestTableToolbar search={search} setSearch={setSearch} enrollments={pendingEnrollments}>
{(canApprove || canDelete) && (
<ToolbarItem>
Expand All @@ -113,7 +120,7 @@ const EnrollmentRequestList = ({ refetchDevices, isStandalone }: EnrollmentReque
</EnrollmentRequestTableToolbar>
<Table
aria-label={t('Table for devices pending approval')}
loading={!!isStandalone && isLoading && isLastUnfilteredListEmpty}
loading={!!isStandalone && isInitialUnfilteredLoad}
columns={enrollmentColumns}
emptyData={itemCount === 0}
clearFilters={() => setSearch('')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ const EnrollmentRequestTableToolbar = ({
<ToolbarContent>
<ToolbarGroup>
<ToolbarItem>
<TableTextSearch value={search} setValue={setSearch} placeholder={t('Search by name')} />
<TableTextSearch
value={search}
setValue={setSearch}
placeholder={t('Search by name')}
inputProps={{ 'data-testid': 'pending-enrollment-request-search-input' }}
/>
</ToolbarItem>
</ToolbarGroup>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,18 @@ export const usePendingEnrollments = (
] => {
const { currentPage, setCurrentPage, itemCount, nextContinue, onPageFetched } =
useTablePagination<EnrollmentRequestList>();

const previousSearch = React.useRef(search);
React.useLayoutEffect(() => {
if (previousSearch.current !== search) {
previousSearch.current = search;
setCurrentPage(1);
}
}, [search, setCurrentPage]);

const [pendingErEndpoint, isDebouncing] = useEnrollmentRequestsEndpoint({ search, nextContinue });

const [erList, isLoading, error, refetch] = useFetchPeriodically<EnrollmentRequestList>(
const [erList, isLoading, error, refetch, updating] = useFetchPeriodically<EnrollmentRequestList>(
{
endpoint: pendingErEndpoint,
},
Expand All @@ -68,5 +77,5 @@ export const usePendingEnrollments = (
[currentPage, setCurrentPage, itemCount],
);

return [erList?.items || [], isLoading || isDebouncing, error, refetch, pagination];
return [erList?.items || [], isLoading || isDebouncing || updating, error, refetch, pagination];
};
5 changes: 3 additions & 2 deletions libs/ui-components/src/components/ListPage/ListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ type ListPageProps = {
description?: string;
headingLevel?: TitleProps['headingLevel'];
children: React.ReactNode;
testId?: string;
};

const ListPage: React.FC<ListPageProps> = ({ title, description, headingLevel = 'h1', children }) => {
const ListPage: React.FC<ListPageProps> = ({ title, description, headingLevel = 'h1', children, testId }) => {
return (
<PageSection hasBodyWrapper={false}>
<PageSection hasBodyWrapper={false} data-testid={testId}>
<Stack hasGutter>
<StackItem>
<Title headingLevel={headingLevel} size="3xl" data-testid="list-page-title">
Expand Down
5 changes: 3 additions & 2 deletions libs/ui-components/src/components/ListPage/ListPageBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ type ListPageBodyProps = {
error: unknown;
loading: boolean;
children: React.ReactNode;
loadingTestId?: string;
};

const ListPageBody: React.FC<ListPageBodyProps> = ({ error, loading, children }) => {
const ListPageBody: React.FC<ListPageBodyProps> = ({ error, loading, children, loadingTestId }) => {
const { t } = useTranslation();
if (error) {
return (
Expand All @@ -24,7 +25,7 @@ const ListPageBody: React.FC<ListPageBodyProps> = ({ error, loading, children })
if (loading) {
return (
<Bullseye>
<Spinner />
<Spinner data-testid={loadingTestId} />
</Bullseye>
);
}
Expand Down
Loading