diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json index 82483ecc1a8b..c2886de3f13f 100644 --- a/openmetadata-ui/src/main/resources/ui/package.json +++ b/openmetadata-ui/src/main/resources/ui/package.json @@ -166,7 +166,7 @@ "react-quill-new": "^3.4.0", "react-reflex": "^4.1.0", "react-resizable": "^3.1.3", - "react-router-dom": "^6.30.4", + "react-router-dom": "7.18.1", "reactflow": "^11.10.2", "reactjs-localstorage": "^1.0.1", "recharts": "2.10.3", diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts index 55de2563f045..a33e8cb3d6ca 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterArticles.spec.ts @@ -632,12 +632,15 @@ test.describe('Context Center Articles', () => { .getByTestId(`knowledge-card-${articleEntity.responseData.displayName}`); await expect(viewedCard).toBeVisible(); + const knowledgePageResponse = page.waitForResponse((response) => + response.url().includes('/api/v1/contextCenter/pages/name/') + ); await viewedCard.getByTestId('knowledge-page-link').first().click(); await page.waitForURL((url) => url.pathname.includes('/context-center/articles/') ); + await knowledgePageResponse; await waitForAllLoadersToDisappear(page); - await page.waitForTimeout(500); await navigateToArticles(page); const rightPanel = page.getByTestId('knowledge-center-right-panel'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts index 6e88c8dc1858..770f4af370d0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts @@ -43,6 +43,7 @@ import { validateLeftSidebarWithHiddenItems, } from '../../utils/customizeNavigation'; import { + clickTabAndWaitForPanel, getEncodedFqn, waitForAllLoadersToDisappear, } from '../../utils/entity'; @@ -498,7 +499,15 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await entity?.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); - + await waitForAllLoadersToDisappear(userPage, 'table-loading'); + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); + await userPage.waitForLoadState('domcontentloaded'); + await userPage + .getByTestId('asset-description-container') + .waitFor({ state: 'visible' }); await expect( userPage.getByRole('tab', { name: 'Custom Tab' }) ).toBeVisible(); @@ -508,8 +517,18 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - await customTab.focus(); - await userPage.keyboard.press('Enter'); + // waitForLoadState('domcontentloaded') is a no-op on this SPA tab + // switch and guarded nothing against react-router v7's deferred + // navigate(); wait for the tab's own panel to become active instead. + await clickTabAndWaitForPanel(userPage, customTab); + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); + await userPage + .getByTestId('asset-description-container') + .locator('visible=true') + .waitFor({ state: 'visible' }); await expect .poll(async () => @@ -652,8 +671,15 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await entity?.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); - await waitForAllLoadersToDisappear(userPage); - + await waitForAllLoadersToDisappear(userPage, 'table-loading'); + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); + await userPage.waitForLoadState('domcontentloaded'); + await userPage + .getByTestId('asset-description-container') + .waitFor({ state: 'visible' }); await expect( userPage.getByRole('tab', { name: 'Custom Tab' }) ).toBeVisible(); @@ -663,8 +689,18 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .last() .getByRole('tab', { name: 'Custom Tab' }); - await customTab.focus(); - await userPage.keyboard.press('Enter'); + // waitForLoadState('domcontentloaded') is a no-op on this SPA tab + // switch and guarded nothing against react-router v7's deferred + // navigate(); wait for the tab's own panel to become active instead. + await clickTabAndWaitForPanel(userPage, customTab); + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); + await userPage + .getByTestId('asset-description-container') + .locator('visible=true') + .waitFor({ state: 'visible' }); await expect .poll(async () => diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts index 22f05a44b8a8..8b1efad01b97 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataProductPersonaCustomization.spec.ts @@ -12,9 +12,9 @@ */ import { APIRequestContext, + test as base, expect, Page, - test as base, } from '@playwright/test'; import { ECustomizedGovernance } from '../../constant/customizeDetail'; import { GlobalSettingOptions } from '../../constant/settings'; @@ -214,12 +214,18 @@ test.describe('Data Product Persona customization', () => { await entity?.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); - + await userPage.waitForLoadState('domcontentloaded'); + await userPage.getByTestId('asset-description-container').waitFor({ state: 'visible' }); await expect( userPage.getByRole('tab', { name: 'Custom Tab' }) ).toBeVisible(); + await userPage.getByRole('tab', { name: 'Custom Tab' }).click(); + + await waitForAllLoadersToDisappear(userPage); + await userPage.waitForLoadState('domcontentloaded'); + await userPage.getByTestId('asset-description-container').locator('visible=true').waitFor({ state: 'visible' }); const visibleDescription = userPage .getByTestId(/KnowledgePanel.Description-/) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestLibrary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestLibrary.spec.ts index 6233cb1a03d3..cf46fb8ba756 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestLibrary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/TestLibrary.spec.ts @@ -18,7 +18,10 @@ import { toastNotification, uuid, } from '../../../utils/common'; -import { fillDeleteConfirmationIfPresent } from '../../../utils/entity'; +import { + fillDeleteConfirmationIfPresent, + waitForAllLoadersToDisappear, +} from '../../../utils/entity'; import { findSystemTestDefinition } from '../../../utils/testCases'; const TEST_DEFINITION_NAME = `AaroCustomTestDefinition${uuid()}`; @@ -1212,6 +1215,7 @@ test.describe( ); await nextButton.click(); await fetchResponse; + await waitForAllLoadersToDisappear(page); // Check again after page load isItemVisible = await testDefLocator.isVisible(); @@ -1256,6 +1260,7 @@ test.describe( expect(updateResponse.status()).toBe(200); await toastNotification(page, /updated successfully/i); + await waitForAllLoadersToDisappear(page); // Verify we stayed on the same page (previous button state should be unchanged) if (prevDisabledBefore) { @@ -1296,6 +1301,7 @@ test.describe( // Wait for the GET that happens after delete (page reset + fetch) await getResponse; + await waitForAllLoadersToDisappear(page); await toastNotification(page, /deleted successfully/i); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts index 6b20cb6dd0b9..1ca7a971558b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ImpactAnalysis.spec.ts @@ -1129,10 +1129,18 @@ test.describe('Impact Analysis', () => { const downstreamCount = await page.locator('[data-row-key]').count(); + const upstreamResponse = page.waitForResponse( + `/api/v1/lineage/getLineageByEntityCount?*` + ); await page.getByRole('radio', { name: 'Upstream' }).click(); + await upstreamResponse; await waitForAllLoadersToDisappear(page); + const downstreamResponse = page.waitForResponse( + `/api/v1/lineage/getLineageByEntityCount?*` + ); await page.getByRole('radio', { name: 'Downstream' }).click(); + await downstreamResponse; await waitForAllLoadersToDisappear(page); const finalCount = await page.locator('[data-row-key]').count(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts index 542849336f37..877928035cf5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts @@ -227,13 +227,27 @@ test.describe('Data Products', () => { await test.step('Navigate to Data Products page', async () => { await sidebarClick(page, SidebarItem.DATA_PRODUCT); await waitForAllLoadersToDisappear(page); + await page.waitForLoadState('domcontentloaded'); }); await test.step('Search for specific data product', async () => { + // The listing page debounces search input by 300ms before firing + // /api/v1/search/query — waitForAllLoadersToDisappear alone doesn't + // guarantee that debounced request has even been sent yet, so assertions + // right after can still see the pre-search (unfiltered) list. + await page + .getByRole('main') + .getByPlaceholder('Search').waitFor({ state: 'visible' }); + const searchResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/search/query') && + response.url().includes(dataProduct1.data.name) + ); await page .getByRole('main') .getByPlaceholder('Search') .fill(dataProduct1.data.name); + await searchResponse; await waitForAllLoadersToDisappear(page); await expect(page.getByText(dataProduct1.data.displayName)).toBeVisible(); @@ -243,7 +257,11 @@ test.describe('Data Products', () => { }); await test.step('Clear search', async () => { + const clearResponse = page.waitForResponse((response) => + response.url().includes('/api/v1/search/query') + ); await page.getByRole('main').getByPlaceholder('Search').clear(); + await clearResponse; await waitForAllLoadersToDisappear(page); await expect(page.getByLabel('Pagination Navigation')).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts index 0539037ea7d5..cb2f18ff8cf4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DescriptionVisibility.spec.ts @@ -41,7 +41,10 @@ import { verifyDescriptionRequiresScroll, verifyEndOfDescriptionReachable, } from '../../utils/domain'; -import { waitForAllLoadersToDisappear } from '../../utils/entity'; +import { + clickTabAndWaitForPanel, + waitForAllLoadersToDisappear, +} from '../../utils/entity'; import { navigateToPersonaWithPagination } from '../../utils/persona'; import { settingClick, sidebarClick } from '../../utils/sidebar'; import { test } from '../fixtures/pages'; @@ -409,15 +412,29 @@ test.describe( await table.visitEntityPage(userPage); await waitForAllLoadersToDisappear(userPage); - - await expect( - userPage.getByRole('tab', { name: 'Description Tab' }) - ).toBeVisible(); - await userPage.getByRole('tab', { name: 'Description Tab' }).click(); + await userPage + .getByTestId('asset-description-container') + .waitFor({ state: 'visible' }); + await userPage.waitForLoadState('domcontentloaded'); + const descriptionTab = userPage.getByRole('tab', { + name: 'Description Tab', + }); + await expect(descriptionTab).toBeVisible(); + // waitForLoadState('domcontentloaded') is a no-op here — an SPA tab click + // triggers no real navigation, so it resolved instantly and guarded + // nothing against react-router v7's deferred navigate(). Wait for the + // active tabpanel instead so the old "Columns" tab content can't still be + // the thing this assertion matches against. + await clickTabAndWaitForPanel(userPage, descriptionTab); + await waitForAllLoadersToDisappear( + userPage, + 'entity-detail-widget-skeleton' + ); const descriptionWidget = userPage .getByTestId(/KnowledgePanel.Description-/) .locator('visible=true'); + await descriptionWidget.waitFor({ state: 'visible' }); await expect(descriptionWidget).toBeVisible(); // Widget truncates long content behind a "more" button @@ -427,6 +444,17 @@ test.describe( await expect(moreButton).toBeVisible(); await moreButton.click(); + // The click flips readMore state, which swaps the BlockEditor (Tiptap) + // content prop from the truncated preview to the full text — but Tiptap + // applies content changes asynchronously (an effect-gated setTimeout, + // then a transaction event deferred behind two requestAnimationFrames), + // so the full text isn't guaranteed to be in the DOM immediately after + // the click. The button's own testid flipping to read-less-button is a + // direct signal that React has re-rendered with the expanded state. + await descriptionWidget + .getByTestId('read-less-button') + .waitFor({ state: 'visible' }); + await verifyEndOfDescriptionReachable(descriptionWidget, userPage); await userPage.close(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 27120b4df830..a39d48232a45 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -2882,7 +2882,9 @@ test.describe('Glossary tests', () => { // Open delete modal await page.click('[data-testid="manage-button"]'); - await page.click('[data-testid="delete-button"]'); + const deleteButton = page.locator('[data-testid="delete-button"]'); + await expect(deleteButton).toBeVisible(); + await deleteButton.click(); // Verify delete modal is visible await expect(page.locator('[role="dialog"]')).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts index 890f859526fa..8b4adbfbed3f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts @@ -356,7 +356,9 @@ test.describe('Lineage Filters', () => { await impactAnalysisTab.scrollIntoViewIfNeeded(); await impactAnalysisTab.click(); await waitForAllLoadersToDisappear(page); - + await page.getByTestId( + `search-dropdown-${filterTestId}` + ).waitFor({ state: 'detached' }); await page.getByTestId('filters-button').click(); await page.getByTestId(`search-dropdown-${filterTestId}`).click(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts index aed5ea0cb1f7..2058c1ead926 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Policies.spec.ts @@ -224,6 +224,14 @@ test.describe( // Click on add rule button await page.locator('[data-testid="add-rule"]').click(); + // add-rule navigates to AddRulePage, a real route change deferred + // under react-router v7's startTransition — without waiting for the + // new page's own marker, addRule()'s fill below can still hit + // [data-testid="rule-name"] on the *previous* page (a read-only span + // for an existing rule, reusing the same testid as the new page's + // input) while the navigation is still pending. + await page.getByTestId('add-rule-title').waitFor({ state: 'visible' }); + // Add rule (assuming addRule is a function you have defined elsewhere) await addRule(page, NEW_RULE_NAME, NEW_RULE_DESCRIPTION, 0); @@ -259,6 +267,13 @@ test.describe( // Click on edit rule button await page.locator('[data-testid="edit-rule"]').click(); + // edit-rule navigates to EditRulePage, a real route change deferred + // under react-router v7's startTransition — wait for the new page's + // own marker before touching [data-testid="rule-name"], which is + // otherwise ambiguous with the read-only rule-name span still on the + // previous page during the pending navigation. + await page.getByTestId('edit-rule-title').waitFor({ state: 'visible' }); + // Enter new name await page.locator('[data-testid="rule-name"]').fill(UPDATED_RULE_NAME); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 9a85d13bff5b..bf54be1c331c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -1131,6 +1131,7 @@ export const testPaginationNavigation = async ( await expect(page.getByTestId('previous')).toBeEnabled(); let afterValue: string | null = ''; if (validateUrl) { + await page.waitForURL((url) => url.searchParams.get('currentPage') === '2'); const currentUrl = page.url(); const urlObj = new URL(currentUrl); const searchParams = urlObj.searchParams; @@ -1164,9 +1165,7 @@ export const testPaginationNavigation = async ( await expect(page.getByTestId('previous')).toBeEnabled(); const paginationText = page.locator('[data-testid="page-indicator"]'); await expect(paginationText).toBeVisible(); - const paginationTextContent = await paginationText.textContent(); - - expect(paginationTextContent).toMatch(/2\s*of\s*\d+/); + await expect(paginationText).toHaveText(/2\s*of\s*\d+/); if (validateUrl) { const reloadedUrl = page.url(); @@ -1177,9 +1176,11 @@ export const testPaginationNavigation = async ( expect(reloadedSearchParams.get('cursorType')).toBe('after'); expect(reloadedSearchParams.get('cursorValue')).toBe(afterValue); } - await page.waitForLoadState('domcontentloaded'); const pageSizeDropdown = page.getByTestId('page-size-selection-dropdown'); - if (await pageSizeDropdown.isVisible()) { + const hasDropdown = (await pageSizeDropdown.count()) > 0; + + if (hasDropdown) { + await expect(pageSizeDropdown).toBeVisible(); await expect(pageSizeDropdown).toHaveText('15 / Page'); // Explicitly using selector, as in some cases table cell contains markdown @@ -1207,11 +1208,25 @@ export const testPaginationNavigation = async ( await pageSizeChangePromise; await waitForAllLoadersToDisappear(page); + await page.waitForURL((url) => url.searchParams.get('pageSize') === '25'); + await waitForAllLoadersToDisappear(page); + await expect(pageSizeDropdown).toHaveText('25 / Page'); - const newRowCount = await page - .locator('tbody > tr[data-row-key]:visible') - .count(); + // The page-size API response resolving and the URL settling to + // pageSize=25 don't guarantee React has committed the re-render that + // grows the table under react-router v7's deferred navigate() — a + // one-shot count() can still catch the stale 15-row DOM in that window. + // Poll until the row count actually reflects the change before reading + // it for the assertions below. + const rowsLocator = page.locator('tbody > tr[data-row-key]:visible'); + if (validateRowCount) { + await expect + .poll(() => rowsLocator.count(), { timeout: 15_000 }) + .not.toBe(initialRowCount); + } + + const newRowCount = await rowsLocator.count(); if (validateRowCount) { expect(newRowCount).toBeLessThanOrEqual(25); expect(newRowCount).not.toBe(initialRowCount); @@ -1483,10 +1498,10 @@ export const testCompletePaginationWithSearch = async ( await waitForAllLoadersToDisappear(page); await expect(page.getByTestId('previous')).toBeEnabled(); + await page.waitForURL((url) => url.searchParams.get('currentPage') === '2'); const paginationPage2 = page.locator('[data-testid="page-indicator"]'); await expect(paginationPage2).toBeVisible(); - const page2Content = await paginationPage2.textContent(); - expect(page2Content).toMatch(/2\s*of\s*\d+/); + await expect(paginationPage2).toHaveText(/2\s*of\s*\d+/); const searchResponsePromise = page.waitForResponse((response) => response.url().includes(searchApiPattern) @@ -1496,14 +1511,16 @@ export const testCompletePaginationWithSearch = async ( const searchResponse = await searchResponsePromise; expect(searchResponse.status()).toBe(200); + await page.waitForURL( + (url) => url.searchParams.get(searchParamName) === searchTestTerm + ); const urlAfterSearch = new URL(page.url()); expect(urlAfterSearch.searchParams.get(searchParamName)).toBe(searchTestTerm); await expect(page.getByTestId('previous')).toBeDisabled(); const paginationAfterSearch = page.locator('[data-testid="page-indicator"]'); await expect(paginationAfterSearch).toBeVisible(); - const searchPage1Content = await paginationAfterSearch.textContent(); - expect(searchPage1Content).toMatch(/1\s*of\s*\d+/); + await expect(paginationAfterSearch).toHaveText(/1\s*of\s*\d+/); const nextButtonAfterSearch = page.locator('[data-testid="next"]'); @@ -1516,10 +1533,10 @@ export const testCompletePaginationWithSearch = async ( expect(searchPage2Response.status()).toBe(200); await expect(page.getByTestId('previous')).toBeEnabled(); + await page.waitForURL((url) => url.searchParams.get('currentPage') === '2'); const paginationSearchPage2 = page.locator('[data-testid="page-indicator"]'); await expect(paginationSearchPage2).toBeVisible(); - const searchPage2Content = await paginationSearchPage2.textContent(); - expect(searchPage2Content).toMatch(/2\s*of\s*\d+/); + await expect(paginationSearchPage2).toHaveText(/2\s*of\s*\d+/); const reloadPromise = page.waitForResponse((response) => response.url().includes(searchApiPattern) @@ -1537,8 +1554,7 @@ export const testCompletePaginationWithSearch = async ( await expect(page.getByTestId('previous')).toBeEnabled(); const paginationAfterRefresh = page.locator('[data-testid="page-indicator"]'); await expect(paginationAfterRefresh).toBeVisible(); - const refreshPage2Content = await paginationAfterRefresh.textContent(); - expect(refreshPage2Content).toMatch(/2\s*of\s*\d+/); + await expect(paginationAfterRefresh).toHaveText(/2\s*of\s*\d+/); await expect(page.getByTestId('searchbar')).toHaveValue(searchTestTerm || ''); @@ -1569,10 +1585,11 @@ export const testCompletePaginationWithSearch = async ( '[data-testid="page-indicator"]' ); await expect(paginationAfterToggleWithSearch).toBeVisible(); - const toggleSearchContent = - await paginationAfterToggleWithSearch.textContent(); - expect(toggleSearchContent).toMatch(/1\s*of\s*\d+/); + await expect(paginationAfterToggleWithSearch).toHaveText(/1\s*of\s*\d+/); + await page.waitForURL( + (url) => url.searchParams.get(searchParamName) === searchTestTerm + ); const urlAfterToggle = new URL(page.url()); expect(urlAfterToggle.searchParams.get(searchParamName)).toBe( searchTestTerm diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts index 90bfe6ad92a8..4e463535dbb5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts @@ -245,15 +245,45 @@ export const removeCertificationFromWidget = async ( await expect(page.getByTestId('add-certification')).toBeVisible(); }; +/** + * Clicks whichever of add-domain / edit-domain is currently showing. A prior + * domain assignment triggers a PATCH-driven re-render that swaps one button for + * the other, but that re-render is deferred under react-router v7's + * startTransition — a single isVisible() check right after can still see the + * stale button. Poll until exactly one of the two is actually visible. + */ +const clickAddOrEditDomainButton = async (page: Page) => { + const addBtn = page.getByTestId('add-domain'); + const editBtn = page.getByTestId('edit-domain'); + + let visibility: { addVisible: boolean; editVisible: boolean } = { + addVisible: false, + editVisible: false, + }; + + await expect + .poll( + async () => { + visibility = { + addVisible: await addBtn.isVisible(), + editVisible: await editBtn.isVisible(), + }; + + return visibility.addVisible || visibility.editVisible; + }, + { timeout: 15_000 } + ) + .toBe(true); + + await (visibility.addVisible ? addBtn : editBtn).click(); +}; + export const assignDomainWidget = async ( page: Page, domain: { name: string; displayName: string; fullyQualifiedName?: string }, multiSelect = false ) => { - const addBtn = page.getByTestId('add-domain'); - const editBtn = page.getByTestId('edit-domain'); - const isAdd = await addBtn.isVisible(); - await (isAdd ? addBtn : editBtn).click(); + await clickAddOrEditDomainButton(page); await waitForAllLoadersToDisappear(page); const searchDomain = page.waitForResponse( @@ -296,10 +326,7 @@ export const removeDomainWidget = async ( page: Page, domain: { name: string; displayName: string; fullyQualifiedName?: string } ) => { - const addBtn = page.getByTestId('add-domain'); - const editBtn = page.getByTestId('edit-domain'); - const isAdd = await addBtn.isVisible(); - await (isAdd ? addBtn : editBtn).click(); + await clickAddOrEditDomainButton(page); await waitForAllLoadersToDisappear(page); await page @@ -1706,6 +1733,7 @@ export const waitForPortRow = async (page: Page, portId: string) => { await page.reload({ waitUntil: 'domcontentloaded' }); await waitForAllLoadersToDisappear(page); + await page.getByTestId('input-output-ports-tab').waitFor({ state: 'visible' }); if (!(await page.getByTestId('input-output-ports-tab').isVisible())) { await navigateToPortsTab(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index 8dc4b4b8396c..2009de147c7d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -59,6 +59,61 @@ export const waitForAllLoadersToDisappear = async ( await expect(loaders).toHaveCount(0, { timeout }); }; +/** + * Clicks a tab and waits for react-router v7's deferred navigate() (wrapped in + * React.startTransition) to actually commit before returning — otherwise callers can + * assert against the previous tab's still-mounted content. Optionally waits for an API + * response tied to the new tab's data. + */ +export const clickTabAndWaitForPanel = async ( + page: Page, + tab: Locator, + options?: { + urlIncludes?: string; + responseMatcher?: (response: Response) => boolean; + } +) => { + const responsePromise = options?.responseMatcher + ? page.waitForResponse(options.responseMatcher) + : undefined; + + await tab.click(); + + if (options?.urlIncludes) { + const urlIncludes = options.urlIncludes; + await page.waitForURL( + (url) => + url.pathname.includes(urlIncludes) || url.search.includes(urlIncludes) + ); + } + + if (responsePromise) { + await responsePromise; + } + + await waitForAllLoadersToDisappear(page); + + // aria-selected flips only once activeKey has actually caught up to the + // click — expect() polls until this settles, which is what actually closes + // the react-router v7 startTransition race (no fixed wait/frame count needed). + await expect(tab).toHaveAttribute('aria-selected', 'true'); + + // Re-read aria-controls on every poll rather than once: a single read right + // after aria-selected flips can still catch a transient/empty value before + // the panel wiring itself has settled, since the two attributes don't + // necessarily commit in the same render. + await expect + .poll(async () => { + const panelId = await tab.getAttribute('aria-controls'); + const panel = panelId + ? page.locator(`#${panelId}`) + : page.locator('[role="tabpanel"][aria-hidden="false"]').first(); + + return panel.isVisible(); + }) + .toBe(true); +}; + export const visitEntityPage = async (data: { page: Page; searchTerm: string; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx index 0d741987b021..c384ae138d7e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx @@ -154,7 +154,6 @@ export const GenericProvider = >({ // Sync selected column from prop (deep link) useEffect(() => { - // If we have a direct columnFqn from props, try to find and select it if (columnFqn && extractedColumns.length > 0) { const col = findFieldByFQN(extractedColumns as Column[], columnFqn); if (col) { @@ -253,8 +252,11 @@ export const GenericProvider = >({ (column: ColumnOrTask) => { const columnFqn = column.fullyQualifiedName; - // If the column is already selected, don't do anything to avoid loops - if (selectedColumn?.fullyQualifiedName === columnFqn) { + // Read via ref so this callback is not recreated when selectedColumn changes. + // Without this, closeColumnDetailPanel (setSelectedColumn null) would recreate + // this callback, causing useFqnDeepLink to re-fire with stale URL params and + // reopen the panel (react-router v7 defers navigate() via startTransition). + if (selectedColumnRef.current?.fullyQualifiedName === columnFqn) { return; } @@ -284,8 +286,8 @@ export const GenericProvider = >({ tab, navigate, location.pathname, - selectedColumn?.fullyQualifiedName, - extractedColumns, + extractedColumns, + // selectedColumn?.fullyQualifiedName intentionally omitted — read via selectedColumnRef ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx index a11eb5ce6013..35b922580dfd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx @@ -18,10 +18,12 @@ import { GRID_VERTICAL_MARGIN, } from '../../../constants/CustomizeWidgets.constants'; import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../../enums/entity.enum'; import { PageType } from '../../../generated/system/ui/page'; import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; import { getWidgetsFromKey } from '../../../utils/CustomizePage/CustomizePageDispatchUtils'; +import { useRequiredParams } from '../../../utils/useRequiredParams'; import { useGenericContext } from '../GenericProvider/GenericContext'; import { DynamicHeightWidget } from './DynamicHeightWidget'; import './generic-tab.less'; @@ -41,6 +43,11 @@ interface GenericTabProps { export const GenericTab = ({ type, variant = 'default' }: GenericTabProps) => { const { layout, updateWidgetHeight } = useGenericContext(); + // react-router v7 defers navigate() via React.startTransition, so this param can + // briefly lag the AntD tab pane that just became active while AntD keeps the + // previous pane mounted. Keying the grid on `tab` forces a clean remount instead + // of patching stale widgets from the previous tab in place. + const { tab } = useRequiredParams<{ tab: EntityTabs }>(); const handleHeightChange = useCallback( (widgetId: string, newHeight: number) => { @@ -103,6 +110,7 @@ export const GenericTab = ({ type, variant = 'default' }: GenericTabProps) => { containerPadding={[1, 0]} isDraggable={false} isResizable={false} + key={tab} margin={[GRID_VERTICAL_MARGIN, GRID_VERTICAL_MARGIN]} preventCollision={false} rowHeight={GRID_ROW_HEIGHT}> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelsTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelsTable.tsx index 234298aaa000..b41ee270aff1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelsTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelsTable.tsx @@ -82,13 +82,6 @@ const DataModelTable = ({ const [isLoading, setIsLoading] = useState(true); const { setFilters } = useTableFilters({}); - useEffect(() => { - const urlPage = Number(pagingCursor.currentPage) || INITIAL_PAGING_VALUE; - if (currentPage !== urlPage) { - handlePageChange(urlPage); - } - }, [pagingCursor.currentPage, currentPage, handlePageChange]); - const searchValue = useMemo(() => { const param = location.search; const searchData = QueryString.parse( diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts index f1557f7fd8a5..26f8dd52f4c0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts @@ -16,6 +16,7 @@ import { useCallback, useEffect, useMemo, + useRef, useState, } from 'react'; import { @@ -83,11 +84,21 @@ export const usePaging = (defaultPageSize?: number): UsePagingInterface => { const [currentPage, setCurrentPage] = useState(initialCurrentPage); const [pageSize, setPageSize] = useState(initialPageSize); + // Keep processedPageSize in a ref so the URL-sync effect can use the latest + // value as a fallback without including it in the dependency array. + // Including processedPageSize as a dep would cause the effect to fire when + // handlePageSize calls setPreference (Zustand synchronous update), at which + // point the URL is still stale — resetting pageSize and currentPage back to + // the old values before the deferred navigate commits. + const processedPageSizeRef = useRef(processedPageSize); + processedPageSizeRef.current = processedPageSize; + // Keep pagination in sync when filters or other controls update paging params directly in the URL. useEffect(() => { const nextCurrentPage = Number(urlParams.currentPage) || INITIAL_PAGING_VALUE; - const nextPageSize = Number(urlParams.pageSize) || processedPageSize; + const nextPageSize = + Number(urlParams.pageSize) || processedPageSizeRef.current; setCurrentPage((currentPage) => currentPage === nextCurrentPage ? currentPage : nextCurrentPage @@ -95,7 +106,7 @@ export const usePaging = (defaultPageSize?: number): UsePagingInterface => { setPageSize((pageSize) => pageSize === nextPageSize ? pageSize : nextPageSize ); - }, [processedPageSize, urlParams.currentPage, urlParams.pageSize]); + }, [urlParams.currentPage, urlParams.pageSize]); const pagingCursorUrlParams: PagingUrlParams = useMemo( () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useFqnDeepLink.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useFqnDeepLink.ts index 19bb41f8f69c..bfa8954a0eee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useFqnDeepLink.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useFqnDeepLink.ts @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { findFieldByFQN, getParentKeysToExpand } from '../utils/TablePureUtils'; interface UseFqnDeepLinkProps { @@ -32,6 +32,9 @@ export const useFqnDeepLink = < openColumnDetailPanel, selectedColumn, }: UseFqnDeepLinkProps) => { + const selectedColumnRef = useRef(selectedColumn); + selectedColumnRef.current = selectedColumn; + useEffect(() => { if (!columnPart || !fqn) { return; @@ -47,7 +50,8 @@ export const useFqnDeepLink = < const matchedField = findFieldByFQN(data, fullColumnFqn); if (matchedField) { if ( - selectedColumn?.fullyQualifiedName !== matchedField.fullyQualifiedName + selectedColumnRef.current?.fullyQualifiedName !== + matchedField.fullyQualifiedName ) { openColumnDetailPanel(matchedField); } @@ -57,7 +61,7 @@ export const useFqnDeepLink = < fqn, data, openColumnDetailPanel, - selectedColumn, setExpandedRowKeys, + // selectedColumn intentionally omitted — tracked via ref to avoid re-triggering on close ]); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx index 75df78eb5015..6b36f906a499 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx @@ -239,6 +239,9 @@ function APIEndpointsTab({ }, [searchValue, currentPage, filters.showDeletedEndpoints]); useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } if (searchValue) { return; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx index 3e5b54c44127..055b132162fd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx @@ -104,6 +104,8 @@ const ContextCenterDocumentsPage: FC = () => { const isLoadingMoreRef = useRef(false); const isLoadingMoreFoldersRef = useRef(false); const folderViewRef = useRef(null); + const previewFileRef = useRef(undefined); + previewFileRef.current = previewFile; const fetchGlobalFileCount = useCallback(async () => { try { @@ -300,7 +302,7 @@ const ContextCenterDocumentsPage: FC = () => { useEffect(() => { const documentId = searchParams.get('document'); - if (!documentId || isDocumentsLoading || previewFile) { + if (!documentId || isDocumentsLoading || previewFileRef.current) { return; } const match = allDocuments.find((d) => d.id === documentId); @@ -339,10 +341,12 @@ const ContextCenterDocumentsPage: FC = () => { }, [ allDocuments, isDocumentsLoading, - previewFile, searchParams, t, setSearchParams, + // previewFile intentionally removed — read via previewFileRef to prevent the effect + // from re-firing when the panel closes (previewFile → undefined), which would reopen + // it while searchParams still holds the stale document ID (react-router v7 startTransition) ]); useEffect(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx index d911ebb76d4c..6df76e051d23 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx @@ -131,6 +131,8 @@ const ContextCenterMemoriesPage: FC = () => { const [debouncedSearch, setDebouncedSearch] = useState(''); const [debouncedAuthorSearch, setDebouncedAuthorSearch] = useState(''); const isAuthorSearchMounted = useRef(false); + const isViewModalOpenRef = useRef(isViewModalOpen); + isViewModalOpenRef.current = isViewModalOpen; const SORT_OPTIONS = useMemo( () => [ @@ -455,7 +457,7 @@ const ContextCenterMemoriesPage: FC = () => { useEffect(() => { const memoryName = searchParams.get('memory'); - if (!memoryName || isViewModalOpen) { + if (!memoryName || isViewModalOpenRef.current) { return; } @@ -469,7 +471,10 @@ const ContextCenterMemoriesPage: FC = () => { return prev; }); }); - }, [isViewModalOpen, searchParams, handleViewMemory, setSearchParams]); + }, [searchParams, handleViewMemory, setSearchParams]); + // isViewModalOpen intentionally removed — read via ref to prevent the effect + // from re-firing when the modal closes (isViewModalOpen → false), which would + // reopen it while searchParams still holds the stale memory name (react-router v7 startTransition) const handleModalSuccess = useCallback(() => { handleModalClose(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx index bf12ea30ac40..3dbde7d680d7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx @@ -302,6 +302,9 @@ function SchemaTablesTab({ }, [searchValue, currentPage, showDeletedSchemas]); useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } if (searchValue) { return; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx index b5bfee8103a1..8515fa4bec14 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx @@ -275,6 +275,9 @@ const RolesListPage = () => { }; useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } const { cursorType, cursorValue } = pagingCursor ?? {}; if (cursorType && cursorValue) { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx index f12f8e3cf46e..a7c0729c391d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx @@ -60,13 +60,6 @@ const StoredProcedureTab = () => { const [isLoading, setIsLoading] = useState(true); const { fqn: decodedDatabaseSchemaFQN } = useFqn(); - useEffect(() => { - const urlPage = Number(pagingCursor.currentPage) || INITIAL_PAGING_VALUE; - if (currentPage !== urlPage) { - handlePageChange(urlPage); - } - }, [pagingCursor.currentPage, currentPage, handlePageChange]); - const { filters: tableFilters, setFilters } = useTableFilters( INITIAL_TABLE_FILTERS ); @@ -208,6 +201,9 @@ const StoredProcedureTab = () => { }, [searchValue, currentPage, showDeletedStoredProcedures]); useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } if (searchValue) { return; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx index 784a7bc798a8..e9d85dcc7ec2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx @@ -250,6 +250,9 @@ const UserListPageV1 = () => { }, [searchValue, currentPage, isDeleted]); useEffect(() => { + if (pageSize !== pagingCursor.pageSize) { + return; + } if (searchValue) { return; } diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock index 4d1182fe76ba..7cc1adb37706 100644 --- a/openmetadata-ui/src/main/resources/ui/yarn.lock +++ b/openmetadata-ui/src/main/resources/ui/yarn.lock @@ -2381,11 +2381,6 @@ resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f" integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg== -"@remix-run/router@1.23.3": - version "1.23.3" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.3.tgz#957c098d4393d301a8aa7dccf3ef28ea5430e36a" - integrity sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q== - "@rjsf/core@5.24.13": version "5.24.13" resolved "https://registry.yarnpkg.com/@rjsf/core/-/core-5.24.13.tgz#ef98e5dc6ac064b2be2f56e0887c99df2b1f8d44" @@ -5607,7 +5602,7 @@ cookie-storage@^6.1.0: resolved "https://registry.yarnpkg.com/cookie-storage/-/cookie-storage-6.1.0.tgz#291b2f662d961be44f999626593421cbfcf23790" integrity sha512-HeVqbVy8BjXhAAuFtL6MTG+witHoLbxfky2jgVh9FmxmyL6IKa9gSSyPNjevXCCCxPu6Tzd9J8+eXTRQzYU/cg== -cookie@0.7.0: +cookie@0.7.0, cookie@^1.0.1: version "0.7.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.0.tgz#2148f68a77245d5c2c0005d264bc3e08cfa0655d" integrity sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ== @@ -11462,20 +11457,20 @@ react-resizable@^3.0.5, react-resizable@^3.1.3: prop-types "15.x" react-draggable "^4.5.0" -react-router-dom@^6.30.4: - version "6.30.4" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.30.4.tgz#f7167bf3da6c7d9132130ea985dd06def25e84d5" - integrity sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q== +react-router-dom@7.18.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.18.1.tgz#0d1b138e291393059ad481c3e10e366385a978a4" + integrity sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg== dependencies: - "@remix-run/router" "1.23.3" - react-router "6.30.4" + react-router "7.18.1" -react-router@6.30.4: - version "6.30.4" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.30.4.tgz#638f35176527bd243d96d81d35d33b757bad46c2" - integrity sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA== +react-router@7.18.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.18.1.tgz#61259d1594b95c1ace299ee4c57453570f0c22f1" + integrity sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg== dependencies: - "@remix-run/router" "1.23.3" + cookie "^1.0.1" + set-cookie-parser "^2.6.0" react-shallow-renderer@^16.15.0: version "16.15.0" @@ -12001,6 +11996,11 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"