fetch domains before any widget is loaded (#17695)

* fix domain init ordering

* move domains call at the beginning

* fix tests

* fix tests

* fix flaky tests

* fix flaky tests

(cherry picked from commit 1dc15bc49376d2c117dcc297c05f120f1f595bad)
This commit is contained in:
Karan Hotchandani 2024-09-05 10:05:54 +05:30 committed by karanh37
parent 32a85c0c96
commit d46540ea0f
8 changed files with 74 additions and 492 deletions

View File

@ -1,442 +0,0 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
addTableFieldTags,
deleteEntity,
interceptURL,
removeTableFieldTags,
updateTableFieldDescription,
verifyResponseStatusCode,
} from '../../common/common';
import {
createSingleLevelEntity,
hardDeleteService,
} from '../../common/EntityUtils';
import { visitEntityDetailsPage } from '../../common/Utils/Entity';
import { getToken } from '../../common/Utils/LocalStorage';
import { BASE_URL } from '../../constants/constants';
import { EntityType } from '../../constants/Entity.interface';
import {
POLICY_DETAILS,
ROLE_DETAILS,
SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST,
SEARCH_INDEX_DISPLAY_NAME,
SEARCH_SERVICE_DETAILS,
TAG_1,
UPDATE_FIELD_DESCRIPTION,
USER_CREDENTIALS,
} from '../../constants/SearchIndexDetails.constants';
const performCommonOperations = () => {
// User should be able to edit search index field tags
addTableFieldTags(
SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.fields[0].fullyQualifiedName,
TAG_1.classification,
TAG_1.tag,
'searchIndexes'
);
removeTableFieldTags(
SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.fields[0].fullyQualifiedName,
TAG_1.classification,
TAG_1.tag,
'searchIndexes'
);
// User should be able to edit search index field description
updateTableFieldDescription(
SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.fields[0].fullyQualifiedName,
UPDATE_FIELD_DESCRIPTION,
'searchIndexes'
);
cy.get(
`[data-row-key="${SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.fields[0].fullyQualifiedName}"] [data-testid="description"]`
).contains(UPDATE_FIELD_DESCRIPTION);
updateTableFieldDescription(
SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.fields[0].fullyQualifiedName,
' ',
'searchIndexes'
);
cy.get(
`[data-row-key="${SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.fields[0].fullyQualifiedName}"] [data-testid="description"]`
).contains('No Description');
};
describe(
'SearchIndexDetails page should work properly for data consumer role',
{ tags: 'DataAssets' },
() => {
const data = { user: { id: '' } };
before(() => {
cy.login();
cy.getAllLocalStorage().then((storageData) => {
const token = getToken(storageData);
// Create search index entity
createSingleLevelEntity({
token,
...SEARCH_SERVICE_DETAILS,
});
// Create a new user
cy.request({
method: 'POST',
url: `/api/v1/users/signup`,
headers: { Authorization: `Bearer ${token}` },
body: USER_CREDENTIALS,
}).then((response) => {
data.user = response.body;
});
cy.logout();
});
});
after(() => {
cy.login();
cy.getAllLocalStorage().then((storageData) => {
const token = getToken(storageData);
// Delete search index
hardDeleteService({
token,
serviceFqn: SEARCH_SERVICE_DETAILS.service.name,
serviceType: SEARCH_SERVICE_DETAILS.serviceType,
});
// Delete created user
cy.request({
method: 'DELETE',
url: `/api/v1/users/${data.user.id}?hardDelete=true&recursive=false`,
headers: { Authorization: `Bearer ${token}` },
});
});
});
beforeEach(() => {
// Login with the created user
cy.login(USER_CREDENTIALS.email, USER_CREDENTIALS.password);
cy.url().should('eq', `${BASE_URL}/my-data`);
});
it('All permissible actions on search index details page should work properly', () => {
visitEntityDetailsPage({
term: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.name,
serviceName: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.service,
entity: EntityType.SearchIndex,
});
// Edit domain option should not be available
cy.get(
`[data-testid="entity-page-header"] [data-testid="add-domain"]`
).should('not.exist');
// Manage button should not be visible on service page
cy.get(
'[data-testid="asset-header-btn-group"] [data-testid="manage-button"]'
).should('not.exist');
performCommonOperations();
cy.logout();
});
}
);
describe('SearchIndexDetails page should work properly for data steward role', () => {
const data = {
user: { id: '' },
policy: { id: '' },
role: { id: '', name: '' },
};
before(() => {
cy.login();
cy.getAllLocalStorage().then((storageData) => {
const token = getToken(storageData);
// Create search index entity
createSingleLevelEntity({
token,
...SEARCH_SERVICE_DETAILS,
});
// Create Data Steward Policy
cy.request({
method: 'POST',
url: `/api/v1/policies`,
headers: { Authorization: `Bearer ${token}` },
body: POLICY_DETAILS,
}).then((policyResponse) => {
data.policy = policyResponse.body;
// Create Data Steward Role
cy.request({
method: 'POST',
url: `/api/v1/roles`,
headers: { Authorization: `Bearer ${token}` },
body: ROLE_DETAILS,
}).then((roleResponse) => {
data.role = roleResponse.body;
// Create a new user
cy.request({
method: 'POST',
url: `/api/v1/users/signup`,
headers: { Authorization: `Bearer ${token}` },
body: USER_CREDENTIALS,
}).then((userResponse) => {
data.user = userResponse.body;
// Assign data steward role to the user
cy.request({
method: 'PATCH',
url: `/api/v1/users/${data.user.id}`,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json-patch+json',
},
body: [
{
op: 'add',
path: '/roles/0',
value: {
id: data.role.id,
type: 'role',
name: data.role.name,
},
},
],
});
});
});
});
cy.logout();
});
});
after(() => {
cy.login();
cy.getAllLocalStorage().then((storageData) => {
const token = getToken(storageData);
// Delete created user
cy.request({
method: 'DELETE',
url: `/api/v1/users/${data.user.id}?hardDelete=true&recursive=false`,
headers: { Authorization: `Bearer ${token}` },
});
// Delete policy
cy.request({
method: 'DELETE',
url: `/api/v1/policies/${data.policy.id}?hardDelete=true&recursive=false`,
headers: { Authorization: `Bearer ${token}` },
});
// Delete role
cy.request({
method: 'DELETE',
url: `/api/v1/roles/${data.role.id}?hardDelete=true&recursive=false`,
headers: { Authorization: `Bearer ${token}` },
});
// Delete search index
hardDeleteService({
token,
serviceFqn: SEARCH_SERVICE_DETAILS.service.name,
serviceType: SEARCH_SERVICE_DETAILS.serviceType,
});
});
});
beforeEach(() => {
// Login with the created user
cy.login(USER_CREDENTIALS.email, USER_CREDENTIALS.password);
cy.url().should('eq', `${BASE_URL}/my-data`);
});
it('All permissible actions on search index details page should work properly', () => {
visitEntityDetailsPage({
term: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.name,
serviceName: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.service,
entity: EntityType.SearchIndex,
});
// Edit domain option should not be available
cy.get(`[data-testid="entity-page-header"]`).then(($body) => {
const editDomain = $body.find(`[data-testid="add-domain"]`);
expect(editDomain.length).to.equal(0);
});
// Manage button should be visible on service page
cy.get('[data-testid="manage-button"]').click();
// Announcement and Delete options should not be visible
cy.get('.manage-dropdown-list-container').then(($body) => {
const announcementButton = $body.find(
`[data-testid="announcement-button"]`
);
const deleteButton = $body.find(`[data-testid="delete-button"]`);
expect(announcementButton.length).to.equal(0);
expect(deleteButton.length).to.equal(0);
});
// Rename search index flow should work properly
cy.get('[data-testid="rename-button"]').click({ waitForAnimations: true });
cy.get('#displayName').clear().type(SEARCH_INDEX_DISPLAY_NAME);
interceptURL('PATCH', `/api/v1/searchIndexes/*`, 'updateDisplayName');
cy.get('[data-testid="save-button"]').click();
verifyResponseStatusCode('@updateDisplayName', 200);
cy.get('[data-testid="entity-header-display-name"]').contains(
SEARCH_INDEX_DISPLAY_NAME
);
performCommonOperations();
});
});
describe('SearchIndexDetails page should work properly for admin role', () => {
before(() => {
cy.login();
cy.getAllLocalStorage().then((storageData) => {
const token = getToken(storageData);
// Create search index entity
createSingleLevelEntity({
token,
...SEARCH_SERVICE_DETAILS,
});
});
});
after(() => {
cy.login();
cy.getAllLocalStorage().then((storageData) => {
const token = getToken(storageData);
// Delete search index
hardDeleteService({
token,
serviceFqn: SEARCH_SERVICE_DETAILS.service.name,
serviceType: SEARCH_SERVICE_DETAILS.serviceType,
});
});
});
beforeEach(() => {
cy.login();
});
it('All permissible actions on search index details page should work properly', () => {
visitEntityDetailsPage({
term: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.name,
serviceName: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.service,
entity: EntityType.SearchIndex,
});
performCommonOperations();
});
it('Soft delete workflow should work properly', () => {
visitEntityDetailsPage({
term: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.name,
serviceName: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.service,
entity: EntityType.SearchIndex,
});
deleteEntity(
SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.name,
SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.service,
EntityType.SearchIndex,
'Search Index',
'soft'
);
cy.get('[data-testid="deleted-badge"]').should('be.visible');
// Edit options for domain owner and tier should not be visible
cy.get('[data-testid="add-domain"]').should('not.exist');
cy.get('[data-testid="edit-owner"]').should('not.exist');
cy.get('[data-testid="edit-tier"]').should('not.exist');
// Edit description button should not be visible
cy.get('[data-testid="edit-description"]').should('not.exist');
// Edit tags button should not be visible
cy.get(
`[data-testid="entity-right-panel"] [data-testid="tags-container"] [data-testid="add-tag"]`
).should('not.exist');
// Edit description and tags button for fields should not be visible
cy.get(
`[data-row-key="${SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.fields[0].fullyQualifiedName}"]`
).then(($body) => {
const addTag = $body.find(
`[data-testid="tags-container"] [data-testid="add-tag"]`
);
const editDescription = $body.find(
`[data-testid="description"] [data-testid="edit-button"]`
);
expect(addTag.length).to.equal(0);
expect(editDescription.length).to.equal(0);
// Restore search index flow should work properly
cy.get('[data-testid="manage-button"]').click();
cy.get('[data-testid="restore-button"]').click();
interceptURL(
'PUT',
`/api/v1/searchIndexes/restore`,
'restoreSearchIndex'
);
cy.get('[data-testid="restore-asset-modal"] .ant-btn-primary')
.contains('Restore')
.click();
verifyResponseStatusCode('@restoreSearchIndex', 200);
});
});
it('Hard delete workflow should work properly', () => {
visitEntityDetailsPage({
term: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.name,
serviceName: SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.service,
entity: EntityType.SearchIndex,
});
deleteEntity(
SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.name,
SEARCH_INDEX_DETAILS_FOR_DETAILS_PAGE_TEST.service,
EntityType.SearchIndex,
'Search Index'
);
});
});

View File

@ -181,12 +181,13 @@ test.describe('Domains', () => {
await afterAction();
});
test('Switch domain from navbar and check domain query call warp in quotes', async ({
test('Switch domain from navbar and check domain query call wrap in quotes', async ({
page,
}) => {
const { afterAction, apiContext } = await getApiContext(page);
const domain = new Domain();
await domain.create(apiContext);
await page.reload();
await page.getByTestId('domain-dropdown').click();
await page
.locator(
@ -217,8 +218,8 @@ test.describe('Domains', () => {
const { assets, assetCleanup } = await setupAssetsForDomain(page);
const domain = new Domain();
await domain.create(apiContext);
await sidebarClick(page, SidebarItem.DOMAIN);
await page.reload();
await sidebarClick(page, SidebarItem.DOMAIN);
await addAssetsToDomain(page, domain.data, assets);
await page.getByTestId('documentation').click();
const updatedDomainName = 'PW Domain Updated';
@ -275,10 +276,12 @@ test.describe('Domains Rbac', () => {
test.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction, page } = await performAdminLogin(browser);
await domain1.create(apiContext);
await domain2.create(apiContext);
await domain3.create(apiContext);
await user1.create(apiContext);
await Promise.all([
domain1.create(apiContext),
domain2.create(apiContext),
domain3.create(apiContext),
user1.create(apiContext),
]);
const domainPayload: Operation[] = [
{
@ -322,6 +325,8 @@ test.describe('Domains Rbac', () => {
});
test('Domain Rbac', async ({ browser }) => {
test.slow(true);
const { page, afterAction, apiContext } = await performAdminLogin(browser);
const { page: userPage, afterAction: afterActionUser1 } =
await performUserLogin(browser, user1);
@ -363,6 +368,19 @@ test.describe('Domains Rbac', () => {
.locator('span')
).toBeVisible();
// Visit explore page and verify if domain is passed in the query
const queryRes = userPage.waitForResponse(
'/api/v1/search/query?*index=dataAsset*'
);
await sidebarClick(userPage, SidebarItem.EXPLORE);
await queryRes.then(async (res) => {
const queryString = new URL(res.request().url()).search;
const urlParams = new URLSearchParams(queryString);
const qParam = urlParams.get('q');
await expect(qParam).toContain(`domain.fullyQualifiedName:`);
});
for (const asset of domainAssset2) {
const fqn = encodeURIComponent(
get(asset, 'entityResponseData.fullyQualifiedName', '')
@ -384,10 +402,13 @@ test.describe('Domains Rbac', () => {
await afterActionUser1();
});
await domain1.delete(apiContext);
await domain2.delete(apiContext);
await domain3.delete(apiContext);
await user1.delete(apiContext);
await Promise.all([
domain1.delete(apiContext),
domain2.delete(apiContext),
domain3.delete(apiContext),
user1.delete(apiContext),
]);
await assetCleanup1();
await assetCleanup2();
await afterAction();

View File

@ -179,6 +179,10 @@ entities.forEach((EntityClass) => {
await entity.followUnfollowEntity(page, entityName);
});
test(`Update displayName`, async ({ page }) => {
await entity.renameEntity(page, entity.entity.name);
});
test.afterAll('Cleanup', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await user.delete(apiContext);

View File

@ -182,7 +182,7 @@ export const fillRule = async (
}
await page
.locator(`.ant-select-dropdown [title="${searchData}"]`)
.locator(`.ant-select-dropdown:visible [title="${searchData}"]`)
.click();
}

View File

@ -299,7 +299,10 @@ export const addAssetsToDomain = async (
const searchRes = page.waitForResponse(
`/api/v1/search/query?q=${name}&index=all&from=0&size=25&*`
);
await page.getByTestId('searchbar').fill(name);
await page
.getByTestId('asset-selection-modal')
.getByTestId('searchbar')
.fill(name);
await searchRes;
await page.locator(`[data-testid="table-data-card_${fqn}"] input`).check();
@ -380,14 +383,18 @@ export const setupAssetsForDomain = async (page: Page) => {
const table = new TableClass();
const topic = new TopicClass();
const dashboard = new DashboardClass();
await table.create(apiContext);
await topic.create(apiContext);
await dashboard.create(apiContext);
await Promise.all([
table.create(apiContext),
topic.create(apiContext),
dashboard.create(apiContext),
]);
const assetCleanup = async () => {
await table.create(apiContext);
await topic.create(apiContext);
await dashboard.create(apiContext);
await Promise.all([
table.delete(apiContext),
topic.delete(apiContext),
dashboard.delete(apiContext),
]);
await afterAction();
};

View File

@ -13,7 +13,6 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { getDomainList } from '../../rest/domainAPI';
import { getLimitConfig } from '../../rest/limitsAPI';
import applicationsClassBase from '../Settings/Applications/AppDetails/ApplicationsClassBase';
import AppContainer from './AppContainer';
@ -85,14 +84,4 @@ describe('AppContainer', () => {
expect(getLimitConfig).toHaveBeenCalled();
});
it('should call domain list to cache domains', () => {
render(
<MemoryRouter>
<AppContainer />
</MemoryRouter>
);
expect(getDomainList).toHaveBeenCalled();
});
});

View File

@ -15,11 +15,8 @@ import { Layout } from 'antd';
import classNames from 'classnames';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ES_MAX_PAGE_SIZE } from '../../constants/constants';
import { useLimitStore } from '../../context/LimitsProvider/useLimitsStore';
import { useApplicationStore } from '../../hooks/useApplicationStore';
import { useDomainStore } from '../../hooks/useDomainStore';
import { getDomainList } from '../../rest/domainAPI';
import { getLimitConfig } from '../../rest/limitsAPI';
import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase';
import Appbar from '../AppBar/Appbar';
@ -32,27 +29,11 @@ const AppContainer = () => {
const { i18n } = useTranslation();
const { Header, Sider, Content } = Layout;
const { currentUser } = useApplicationStore();
const { updateDomains, updateDomainLoading } = useDomainStore();
const AuthenticatedRouter = applicationRoutesClass.getRouteElements();
const ApplicationExtras = applicationsClassBase.getApplicationExtension();
const isDirectionRTL = useMemo(() => i18n.dir() === 'rtl', [i18n]);
const { setConfig, bannerDetails } = useLimitStore();
const fetchDomainList = useCallback(async () => {
try {
updateDomainLoading(true);
const { data } = await getDomainList({
limit: ES_MAX_PAGE_SIZE,
fields: 'parent',
});
updateDomains(data);
} catch (error) {
// silent fail
} finally {
updateDomainLoading(false);
}
}, [currentUser]);
const fetchLimitConfig = useCallback(async () => {
try {
const response = await getLimitConfig();
@ -65,7 +46,6 @@ const AppContainer = () => {
useEffect(() => {
if (currentUser?.id) {
fetchDomainList();
fetchLimitConfig();
}
}, [currentUser?.id]);

View File

@ -41,6 +41,7 @@ import { useTranslation } from 'react-i18next';
import { useHistory, useLocation } from 'react-router-dom';
import {
DEFAULT_DOMAIN_VALUE,
ES_MAX_PAGE_SIZE,
REDIRECT_PATHNAME,
ROUTES,
} from '../../../constants/constants';
@ -56,6 +57,7 @@ import { AuthProvider as AuthProviderEnum } from '../../../generated/settings/se
import { useApplicationStore } from '../../../hooks/useApplicationStore';
import { useDomainStore } from '../../../hooks/useDomainStore';
import axiosClient from '../../../rest';
import { getDomainList } from '../../../rest/domainAPI';
import {
fetchAuthenticationConfig,
fetchAuthorizerConfig,
@ -135,7 +137,7 @@ export const AuthProvider = ({
isApplicationLoading,
setApplicationLoading,
} = useApplicationStore();
const { activeDomain } = useDomainStore();
const { updateDomains, updateDomainLoading } = useDomainStore();
const location = useLocation();
const history = useHistory();
@ -183,6 +185,21 @@ export const AuthProvider = ({
return authenticatorRef.current?.renewIdToken();
};
const fetchDomainList = useCallback(async () => {
try {
updateDomainLoading(true);
const { data } = await getDomainList({
limit: ES_MAX_PAGE_SIZE,
fields: 'parent',
});
updateDomains(data);
} catch (error) {
// silent fail
} finally {
updateDomainLoading(false);
}
}, []);
const handledVerifiedUser = () => {
if (!isProtectedRoute(location.pathname)) {
history.push(ROUTES.HOME);
@ -223,6 +240,8 @@ export const AuthProvider = ({
if (res) {
setCurrentUser(res);
setIsAuthenticated(true);
// Fetch domains at the start
await fetchDomainList();
} else {
resetUserDetails();
}
@ -397,6 +416,9 @@ export const AuthProvider = ({
setCurrentUser(res);
}
// Fetch domains at the start
await fetchDomainList();
handledVerifiedUser();
// Start expiry timer on successful login
startTokenExpiryTimer();
@ -470,6 +492,7 @@ export const AuthProvider = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const withDomainFilter = (config: InternalAxiosRequestConfig<any>) => {
const isGetRequest = config.method === 'get';
const activeDomain = useDomainStore.getState().activeDomain;
const hasActiveDomain = activeDomain !== DEFAULT_DOMAIN_VALUE;
const currentPath = window.location.pathname;
const shouldNotIntercept = [