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
17 changes: 17 additions & 0 deletions libs/cypress/e2e/devices/approveEnrollmentRequest.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,21 @@ 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 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 };
20 changes: 20 additions & 0 deletions libs/cypress/pages/DevicesPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ 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"]');
}

enrollmentRequestKebabMenuAction(actionName: string) {
return cy.get('[role="menuitem"]').contains(actionName);
}
Expand Down
60 changes: 54 additions & 6 deletions libs/cypress/support/interceptors/enrollmentRequests.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
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 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(getErList(hasFieldSelector), fieldSelector);
const body = buildErResponse(enrollmentRequests);

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 +48,29 @@ 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 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 @@ -52,7 +52,7 @@ export const usePendingEnrollments = (
useTablePagination<EnrollmentRequestList>();
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 +68,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