mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-29 02:45:25 +00:00
supported scroll in certification popover list (#23534)
This commit is contained in:
parent
bcae472cc0
commit
f934b79d65
@ -532,3 +532,44 @@ export const executeWithRetry = async <T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const readElementInListWithScroll = async (
|
||||||
|
page: Page,
|
||||||
|
locator: Locator,
|
||||||
|
hierarchyElementLocator: Locator
|
||||||
|
) => {
|
||||||
|
const element = locator;
|
||||||
|
|
||||||
|
// Reset scroll position to top before starting pagination
|
||||||
|
await hierarchyElementLocator.hover();
|
||||||
|
await page.mouse.wheel(0, -99999);
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Retry mechanism for pagination
|
||||||
|
let elementCount = await element.count();
|
||||||
|
let retryCount = 0;
|
||||||
|
const maxRetries = 10;
|
||||||
|
|
||||||
|
while (elementCount === 0 && retryCount < maxRetries) {
|
||||||
|
await hierarchyElementLocator.hover();
|
||||||
|
await page.mouse.wheel(0, 1000);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Create fresh locator and check if the article is now visible after this retry
|
||||||
|
const freshArticle = locator;
|
||||||
|
const count = await freshArticle.count();
|
||||||
|
|
||||||
|
// Check if the article is now visible after this retry
|
||||||
|
elementCount = count;
|
||||||
|
|
||||||
|
// If we found the element, validate it and break out of the loop
|
||||||
|
if (count > 0) {
|
||||||
|
await expect(freshArticle).toBeVisible();
|
||||||
|
|
||||||
|
return; // Exit the function early since we found and validated the article
|
||||||
|
}
|
||||||
|
|
||||||
|
retryCount++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -26,6 +26,7 @@ import { TagClass } from '../support/tag/TagClass';
|
|||||||
import {
|
import {
|
||||||
clickOutside,
|
clickOutside,
|
||||||
descriptionBox,
|
descriptionBox,
|
||||||
|
readElementInListWithScroll,
|
||||||
redirectToHomePage,
|
redirectToHomePage,
|
||||||
toastNotification,
|
toastNotification,
|
||||||
uuid,
|
uuid,
|
||||||
@ -486,8 +487,24 @@ export const assignCertification = async (
|
|||||||
certification: TagClass,
|
certification: TagClass,
|
||||||
endpoint: string
|
endpoint: string
|
||||||
) => {
|
) => {
|
||||||
|
const certificationResponse = page.waitForResponse(
|
||||||
|
'/api/v1/tags?parent=Certification&limit=50'
|
||||||
|
);
|
||||||
await page.getByTestId('edit-certification').click();
|
await page.getByTestId('edit-certification').click();
|
||||||
|
await certificationResponse;
|
||||||
|
await page.waitForSelector('.certification-card-popover', {
|
||||||
|
state: 'visible',
|
||||||
|
});
|
||||||
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||||
|
|
||||||
|
await readElementInListWithScroll(
|
||||||
|
page,
|
||||||
|
page.getByTestId(
|
||||||
|
`radio-btn-${certification.responseData.fullyQualifiedName}`
|
||||||
|
),
|
||||||
|
page.locator('[data-testid="certification-cards"] .ant-radio-group')
|
||||||
|
);
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId(`radio-btn-${certification.responseData.fullyQualifiedName}`)
|
.getByTestId(`radio-btn-${certification.responseData.fullyQualifiedName}`)
|
||||||
.click();
|
.click();
|
||||||
@ -503,6 +520,9 @@ export const assignCertification = async (
|
|||||||
|
|
||||||
export const removeCertification = async (page: Page, endpoint: string) => {
|
export const removeCertification = async (page: Page, endpoint: string) => {
|
||||||
await page.getByTestId('edit-certification').click();
|
await page.getByTestId('edit-certification').click();
|
||||||
|
await page.waitForSelector('.certification-card-popover', {
|
||||||
|
state: 'visible',
|
||||||
|
});
|
||||||
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||||
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
||||||
await page.getByTestId('clear-certification').click();
|
await page.getByTestId('clear-certification').click();
|
||||||
|
@ -17,6 +17,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReactComponent as CertificationIcon } from '../../assets/svg/ic-certification.svg';
|
import { ReactComponent as CertificationIcon } from '../../assets/svg/ic-certification.svg';
|
||||||
import { Tag } from '../../generated/entity/classification/tag';
|
import { Tag } from '../../generated/entity/classification/tag';
|
||||||
|
import { Paging } from '../../generated/type/paging';
|
||||||
import { getTags } from '../../rest/tagAPI';
|
import { getTags } from '../../rest/tagAPI';
|
||||||
import { getEntityName } from '../../utils/EntityUtils';
|
import { getEntityName } from '../../utils/EntityUtils';
|
||||||
import { stringToHTML } from '../../utils/StringsUtils';
|
import { stringToHTML } from '../../utils/StringsUtils';
|
||||||
@ -35,59 +36,84 @@ const Certification = ({
|
|||||||
onClose,
|
onClose,
|
||||||
}: CertificationProps) => {
|
}: CertificationProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const popoverRef = useRef<any>(null);
|
const popoverRef = useRef<{ close: () => void } | null>(null);
|
||||||
const [isLoadingCertificationData, setIsLoadingCertificationData] =
|
const [isLoadingCertificationData, setIsLoadingCertificationData] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
const [hasContentLoading, setHasContentLoading] = useState<boolean>(false);
|
||||||
const [certifications, setCertifications] = useState<Array<Tag>>([]);
|
const [certifications, setCertifications] = useState<Array<Tag>>([]);
|
||||||
const [selectedCertification, setSelectedCertification] = useState<string>(
|
const [selectedCertification, setSelectedCertification] = useState<string>(
|
||||||
currentCertificate ?? ''
|
currentCertificate ?? ''
|
||||||
);
|
);
|
||||||
const certificationCardData = useMemo(() => {
|
const [paging, setPaging] = useState<Paging>({} as Paging);
|
||||||
return (
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
<Radio.Group
|
|
||||||
className="h-max-100 overflow-y-auto overflow-x-hidden"
|
|
||||||
value={selectedCertification}>
|
|
||||||
{certifications.map((certificate) => {
|
|
||||||
const tagSrc = getTagImageSrc(certificate.style?.iconURL ?? '');
|
|
||||||
const title = getEntityName(certificate);
|
|
||||||
const { id, fullyQualifiedName, description } = certificate;
|
|
||||||
|
|
||||||
return (
|
const getCertificationData = async (page = 1, append = false) => {
|
||||||
<div
|
if (page === 1) {
|
||||||
className="certification-card-item cursor-pointer"
|
setIsLoadingCertificationData(true);
|
||||||
key={id}
|
} else {
|
||||||
style={{ cursor: 'pointer' }}
|
setHasContentLoading(true);
|
||||||
onClick={() => {
|
}
|
||||||
setSelectedCertification(fullyQualifiedName ?? '');
|
|
||||||
}}>
|
try {
|
||||||
<Radio
|
const response = await getTags({
|
||||||
className="certification-radio-top-right"
|
parent: 'Certification',
|
||||||
data-testid={`radio-btn-${fullyQualifiedName}`}
|
limit: 50,
|
||||||
value={fullyQualifiedName}
|
after: page > 1 ? paging.after : undefined,
|
||||||
/>
|
});
|
||||||
<div className="certification-card-content">
|
|
||||||
{tagSrc ? (
|
const { data, paging: newPaging } = response;
|
||||||
<img alt={title} src={tagSrc} />
|
|
||||||
) : (
|
// Sort certifications with Gold, Silver, Bronze first (only for initial load)
|
||||||
<div className="certification-icon">
|
const sortedData =
|
||||||
<CertificationIcon height={28} width={28} />
|
page === 1
|
||||||
</div>
|
? [...data].sort((a, b) => {
|
||||||
)}
|
const order: Record<string, number> = {
|
||||||
<div>
|
Gold: 0,
|
||||||
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-body">
|
Silver: 1,
|
||||||
{title}
|
Bronze: 2,
|
||||||
</Typography.Paragraph>
|
};
|
||||||
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
|
|
||||||
{stringToHTML(description)}
|
const aName = getEntityName(a);
|
||||||
</Typography.Paragraph>
|
const bName = getEntityName(b);
|
||||||
</div>
|
|
||||||
</div>
|
const aOrder = order[aName] ?? 3;
|
||||||
</div>
|
const bOrder = order[bName] ?? 3;
|
||||||
);
|
|
||||||
})}
|
return aOrder - bOrder;
|
||||||
</Radio.Group>
|
})
|
||||||
);
|
: data;
|
||||||
}, [certifications, selectedCertification]);
|
|
||||||
|
if (append) {
|
||||||
|
setCertifications((prev) => [...prev, ...sortedData]);
|
||||||
|
} else {
|
||||||
|
setCertifications(sortedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPaging(newPaging);
|
||||||
|
setCurrentPage(page);
|
||||||
|
} catch (err) {
|
||||||
|
showErrorToast(
|
||||||
|
err as AxiosError,
|
||||||
|
t('server.entity-fetch-error', {
|
||||||
|
entity: t('label.certification-plural-lowercase'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingCertificationData(false);
|
||||||
|
setHasContentLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = async (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const { currentTarget } = e;
|
||||||
|
const isAtBottom =
|
||||||
|
currentTarget.scrollTop + currentTarget.offsetHeight >=
|
||||||
|
currentTarget.scrollHeight - 1; // -1 for precision tolerance
|
||||||
|
|
||||||
|
if (isAtBottom && paging.after && !hasContentLoading) {
|
||||||
|
await getCertificationData(currentPage + 1, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateCertificationData = async (value?: string) => {
|
const updateCertificationData = async (value?: string) => {
|
||||||
setIsLoadingCertificationData(true);
|
setIsLoadingCertificationData(true);
|
||||||
@ -98,43 +124,60 @@ const Certification = ({
|
|||||||
setIsLoadingCertificationData(false);
|
setIsLoadingCertificationData(false);
|
||||||
popoverRef.current?.close();
|
popoverRef.current?.close();
|
||||||
};
|
};
|
||||||
const getCertificationData = async () => {
|
|
||||||
setIsLoadingCertificationData(true);
|
|
||||||
try {
|
|
||||||
const { data } = await getTags({
|
|
||||||
parent: 'Certification',
|
|
||||||
limit: 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort certifications with Gold, Silver, Bronze first
|
const certificationCardData = useMemo(() => {
|
||||||
const sortedData = [...data].sort((a, b) => {
|
return (
|
||||||
const order: Record<string, number> = {
|
<div
|
||||||
Gold: 0,
|
className="h-max-100 overflow-y-auto overflow-x-hidden"
|
||||||
Silver: 1,
|
onScroll={handleScroll}>
|
||||||
Bronze: 2,
|
<Radio.Group className="w-full" value={selectedCertification}>
|
||||||
};
|
{certifications.map((certificate) => {
|
||||||
|
const tagSrc = getTagImageSrc(certificate.style?.iconURL ?? '');
|
||||||
|
const title = getEntityName(certificate);
|
||||||
|
const { id, fullyQualifiedName, description } = certificate;
|
||||||
|
|
||||||
const aName = getEntityName(a);
|
return (
|
||||||
const bName = getEntityName(b);
|
<div
|
||||||
|
className="certification-card-item cursor-pointer"
|
||||||
const aOrder = order[aName] ?? 3;
|
key={id}
|
||||||
const bOrder = order[bName] ?? 3;
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
return aOrder - bOrder;
|
setSelectedCertification(fullyQualifiedName ?? '');
|
||||||
});
|
}}>
|
||||||
|
<Radio
|
||||||
setCertifications(sortedData);
|
className="certification-radio-top-right"
|
||||||
} catch (err) {
|
data-testid={`radio-btn-${fullyQualifiedName}`}
|
||||||
showErrorToast(
|
value={fullyQualifiedName}
|
||||||
err as AxiosError,
|
/>
|
||||||
t('server.entity-fetch-error', {
|
<div className="certification-card-content">
|
||||||
entity: t('label.certification-plural-lowercase'),
|
{tagSrc ? (
|
||||||
})
|
<img alt={title} src={tagSrc} />
|
||||||
);
|
) : (
|
||||||
} finally {
|
<div className="certification-icon">
|
||||||
setIsLoadingCertificationData(false);
|
<CertificationIcon height={28} width={28} />
|
||||||
}
|
</div>
|
||||||
};
|
)}
|
||||||
|
<div>
|
||||||
|
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-body">
|
||||||
|
{title}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
|
||||||
|
{stringToHTML(description)}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Radio.Group>
|
||||||
|
{hasContentLoading && (
|
||||||
|
<div className="flex justify-center p-2">
|
||||||
|
<Loader size="small" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [certifications, selectedCertification, hasContentLoading, handleScroll]);
|
||||||
|
|
||||||
const handleCloseCertification = async () => {
|
const handleCloseCertification = async () => {
|
||||||
popoverRef.current?.close();
|
popoverRef.current?.close();
|
||||||
@ -143,16 +186,19 @@ const Certification = ({
|
|||||||
|
|
||||||
const onOpenChange = (visible: boolean) => {
|
const onOpenChange = (visible: boolean) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
getCertificationData();
|
getCertificationData(1);
|
||||||
setSelectedCertification(currentCertificate);
|
setSelectedCertification(currentCertificate);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setPaging({} as Paging);
|
||||||
} else {
|
} else {
|
||||||
setSelectedCertification('');
|
setSelectedCertification('');
|
||||||
|
setCertifications([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (popoverProps?.open && certifications.length === 0) {
|
if (popoverProps?.open && certifications.length === 0) {
|
||||||
getCertificationData();
|
getCertificationData(1);
|
||||||
}
|
}
|
||||||
}, [popoverProps?.open]);
|
}, [popoverProps?.open]);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user