mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-12 18:47:45 +00:00
Update Domains state management to improve scaling with many Domains (#14478)
This commit is contained in:
parent
a79b542d65
commit
2cad5ddcb3
@ -62,6 +62,7 @@ export const updateListDomainsCache = (
|
||||
addToListDomainsCache(
|
||||
client,
|
||||
{
|
||||
__typename: 'Domain',
|
||||
urn,
|
||||
id: id || null,
|
||||
type: EntityType.Domain,
|
||||
@ -75,6 +76,8 @@ export const updateListDomainsCache = (
|
||||
dataProducts: null,
|
||||
parentDomains: null,
|
||||
displayProperties: null,
|
||||
institutionalMemory: null,
|
||||
applications: null,
|
||||
},
|
||||
1000,
|
||||
parentDomain,
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import analytics, { EventType } from '@app/analytics';
|
||||
import { useDomainsContext as useDomainsContextV2 } from '@app/domainV2/DomainsContext';
|
||||
import { UpdatedDomain, useDomainsContext as useDomainsContextV2 } from '@app/domainV2/DomainsContext';
|
||||
import DomainParentSelect from '@app/entityV2/shared/EntityDropdown/DomainParentSelect';
|
||||
import { ModalButtonContainer } from '@app/shared/button/styledComponents';
|
||||
import { validateCustomUrnId } from '@app/shared/textUtil';
|
||||
@ -12,6 +12,7 @@ import { useIsNestedDomainsEnabled } from '@app/useAppConfig';
|
||||
import { Button } from '@src/alchemy-components';
|
||||
|
||||
import { useCreateDomainMutation } from '@graphql/domain.generated';
|
||||
import { EntityType } from '@types';
|
||||
|
||||
const SuggestedNamesGroup = styled.div`
|
||||
margin-top: 8px;
|
||||
@ -48,7 +49,7 @@ const AdvancedLabel = styled(Typography.Text)`
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onCreate: (
|
||||
onCreate?: (
|
||||
urn: string,
|
||||
id: string | undefined,
|
||||
name: string,
|
||||
@ -66,7 +67,7 @@ const DESCRIPTION_FIELD_NAME = 'description';
|
||||
export default function CreateDomainModal({ onClose, onCreate }: Props) {
|
||||
const isNestedDomainsEnabled = useIsNestedDomainsEnabled();
|
||||
const [createDomainMutation] = useCreateDomainMutation();
|
||||
const { entityData } = useDomainsContextV2();
|
||||
const { entityData, setNewDomain } = useDomainsContextV2();
|
||||
const [selectedParentUrn, setSelectedParentUrn] = useState<string>(
|
||||
(isNestedDomainsEnabled && entityData?.urn) || '',
|
||||
);
|
||||
@ -94,13 +95,24 @@ export default function CreateDomainModal({ onClose, onCreate }: Props) {
|
||||
content: `Created domain!`,
|
||||
duration: 3,
|
||||
});
|
||||
onCreate(
|
||||
onCreate?.(
|
||||
data?.createDomain || '',
|
||||
form.getFieldValue(ID_FIELD_NAME),
|
||||
form.getFieldValue(NAME_FIELD_NAME),
|
||||
form.getFieldValue(DESCRIPTION_FIELD_NAME),
|
||||
selectedParentUrn || undefined,
|
||||
);
|
||||
const newDomain: UpdatedDomain = {
|
||||
urn: data?.createDomain || '',
|
||||
type: EntityType.Domain,
|
||||
id: form.getFieldValue(ID_FIELD_NAME),
|
||||
properties: {
|
||||
name: form.getFieldValue(NAME_FIELD_NAME),
|
||||
description: form.getFieldValue(DESCRIPTION_FIELD_NAME),
|
||||
},
|
||||
parentDomain: selectedParentUrn || undefined,
|
||||
};
|
||||
setNewDomain(newDomain);
|
||||
form.resetFields();
|
||||
}
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
import { Route, Switch, matchPath, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
import { DomainsContext } from '@app/domainV2/DomainsContext';
|
||||
import { DomainsContext, UpdatedDomain } from '@app/domainV2/DomainsContext';
|
||||
import ManageDomainsPageV2 from '@app/domainV2/nestedDomains/ManageDomainsPageV2';
|
||||
import ManageDomainsSidebar from '@app/domainV2/nestedDomains/ManageDomainsSidebar';
|
||||
import { EntityPage } from '@app/entity/EntityPage';
|
||||
@ -26,6 +26,9 @@ const ContentWrapper = styled.div<{ $isShowNavBarRedesign?: boolean; $isEntityPr
|
||||
export default function DomainRoutes() {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const [entityData, setEntityData] = useState<GenericEntityProperties | null>(null);
|
||||
const [newDomain, setNewDomain] = useState<UpdatedDomain | null>(null);
|
||||
const [deletedDomain, setDeletedDomain] = useState<UpdatedDomain | null>(null);
|
||||
const [updatedDomain, setUpdatedDomain] = useState<UpdatedDomain | null>(null);
|
||||
const [isSidebarClosed, setIsSidebarClosed] = useState(true);
|
||||
const entitySidebarWidth = useSidebarWidth();
|
||||
const isShowNavBarRedesign = useShowNavBarRedesign();
|
||||
@ -35,7 +38,18 @@ export default function DomainRoutes() {
|
||||
matchPath(location.pathname, `/${entityRegistry.getPathName(EntityType.Domain)}/:urn`) !== null;
|
||||
|
||||
return (
|
||||
<DomainsContext.Provider value={{ entityData, setEntityData }}>
|
||||
<DomainsContext.Provider
|
||||
value={{
|
||||
entityData,
|
||||
setEntityData,
|
||||
newDomain,
|
||||
setNewDomain,
|
||||
deletedDomain,
|
||||
setDeletedDomain,
|
||||
updatedDomain,
|
||||
setUpdatedDomain,
|
||||
}}
|
||||
>
|
||||
<ContentWrapper $isShowNavBarRedesign={isShowNavBarRedesign} $isEntityProfile={isEntityProfile}>
|
||||
<ManageDomainsSidebar isEntityProfile={isEntityProfile} />
|
||||
<Switch>
|
||||
|
||||
@ -2,17 +2,32 @@ import React, { useContext } from 'react';
|
||||
|
||||
import { GenericEntityProperties } from '@app/entity/shared/types';
|
||||
|
||||
import { ListDomainFragment } from '@graphql/domain.generated';
|
||||
|
||||
export type UpdatedDomain = ListDomainFragment & { parentDomain?: string };
|
||||
|
||||
export interface DomainsContextType {
|
||||
entityData: GenericEntityProperties | null;
|
||||
setEntityData: (entityData: GenericEntityProperties | null) => void;
|
||||
newDomain: UpdatedDomain | null;
|
||||
setNewDomain: (newDomain: UpdatedDomain | null) => void;
|
||||
deletedDomain: UpdatedDomain | null;
|
||||
setDeletedDomain: (newDomain: UpdatedDomain | null) => void;
|
||||
updatedDomain: UpdatedDomain | null;
|
||||
setUpdatedDomain: (newDomain: UpdatedDomain | null) => void;
|
||||
}
|
||||
|
||||
export const DomainsContext = React.createContext<DomainsContextType>({
|
||||
entityData: null,
|
||||
setEntityData: () => {},
|
||||
newDomain: null,
|
||||
setNewDomain: () => {},
|
||||
deletedDomain: null,
|
||||
setDeletedDomain: () => {},
|
||||
updatedDomain: null,
|
||||
setUpdatedDomain: () => {},
|
||||
});
|
||||
|
||||
export const useDomainsContext = () => {
|
||||
const { entityData, setEntityData } = useContext(DomainsContext);
|
||||
return { entityData, setEntityData };
|
||||
return useContext(DomainsContext);
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import { Typography } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DomainsContext } from '@app/domainV2/DomainsContext';
|
||||
import { DomainsContext, UpdatedDomain } from '@app/domainV2/DomainsContext';
|
||||
import { DomainsList } from '@app/domainV2/DomainsList';
|
||||
import { GenericEntityProperties } from '@app/entity/shared/types';
|
||||
|
||||
@ -26,9 +26,23 @@ const ListContainer = styled.div``;
|
||||
|
||||
export const ManageDomainsPage = () => {
|
||||
const [entityData, setEntityData] = useState<GenericEntityProperties | null>(null);
|
||||
const [newDomain, setNewDomain] = useState<UpdatedDomain | null>(null);
|
||||
const [deletedDomain, setDeletedDomain] = useState<UpdatedDomain | null>(null);
|
||||
const [updatedDomain, setUpdatedDomain] = useState<UpdatedDomain | null>(null);
|
||||
|
||||
return (
|
||||
<DomainsContext.Provider value={{ entityData, setEntityData }}>
|
||||
<DomainsContext.Provider
|
||||
value={{
|
||||
entityData,
|
||||
setEntityData,
|
||||
newDomain,
|
||||
setNewDomain,
|
||||
deletedDomain,
|
||||
setDeletedDomain,
|
||||
updatedDomain,
|
||||
setUpdatedDomain,
|
||||
}}
|
||||
>
|
||||
<PageContainer>
|
||||
<PageHeaderContainer>
|
||||
<PageTitle level={3}>Domains</PageTitle>
|
||||
|
||||
@ -0,0 +1,307 @@
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DomainsContext, UpdatedDomain, useDomainsContext } from '@app/domainV2/DomainsContext';
|
||||
import { GenericEntityProperties } from '@app/entity/shared/types';
|
||||
|
||||
import { EntityType } from '@types';
|
||||
|
||||
// Mock domain for testing
|
||||
const createMockDomain = (urn: string, name: string): UpdatedDomain => ({
|
||||
urn,
|
||||
id: urn,
|
||||
type: EntityType.Domain,
|
||||
properties: {
|
||||
name,
|
||||
description: `Description for ${name}`,
|
||||
},
|
||||
ownership: null,
|
||||
entities: null,
|
||||
children: { total: 0 },
|
||||
dataProducts: null,
|
||||
parentDomains: null,
|
||||
displayProperties: null,
|
||||
});
|
||||
|
||||
// Test component that uses the context
|
||||
const TestComponent = () => {
|
||||
const {
|
||||
entityData,
|
||||
setEntityData,
|
||||
newDomain,
|
||||
setNewDomain,
|
||||
deletedDomain,
|
||||
setDeletedDomain,
|
||||
updatedDomain,
|
||||
setUpdatedDomain,
|
||||
} = useDomainsContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="entity-data">{entityData ? JSON.stringify(entityData) : 'null'}</div>
|
||||
<div data-testid="new-domain">{newDomain ? newDomain.urn : 'null'}</div>
|
||||
<div data-testid="deleted-domain">{deletedDomain ? deletedDomain.urn : 'null'}</div>
|
||||
<div data-testid="updated-domain">{updatedDomain ? updatedDomain.urn : 'null'}</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-testid="set-entity-data"
|
||||
onClick={() => setEntityData({ urn: 'test-urn', type: EntityType.Domain })}
|
||||
>
|
||||
Set Entity Data
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="set-new-domain"
|
||||
onClick={() => setNewDomain(createMockDomain('urn:li:domain:new', 'New Domain'))}
|
||||
>
|
||||
Set New Domain
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="set-deleted-domain"
|
||||
onClick={() => setDeletedDomain(createMockDomain('urn:li:domain:deleted', 'Deleted Domain'))}
|
||||
>
|
||||
Set Deleted Domain
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="set-updated-domain"
|
||||
onClick={() => setUpdatedDomain(createMockDomain('urn:li:domain:updated', 'Updated Domain'))}
|
||||
>
|
||||
Set Updated Domain
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="clear-all"
|
||||
onClick={() => {
|
||||
setEntityData(null);
|
||||
setNewDomain(null);
|
||||
setDeletedDomain(null);
|
||||
setUpdatedDomain(null);
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Test provider component that manages context state
|
||||
const TestProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [entityData, setEntityData] = useState<GenericEntityProperties | null>(null);
|
||||
const [newDomain, setNewDomain] = useState<UpdatedDomain | null>(null);
|
||||
const [deletedDomain, setDeletedDomain] = useState<UpdatedDomain | null>(null);
|
||||
const [updatedDomain, setUpdatedDomain] = useState<UpdatedDomain | null>(null);
|
||||
|
||||
return (
|
||||
<DomainsContext.Provider
|
||||
value={{
|
||||
entityData,
|
||||
setEntityData,
|
||||
newDomain,
|
||||
setNewDomain,
|
||||
deletedDomain,
|
||||
setDeletedDomain,
|
||||
updatedDomain,
|
||||
setUpdatedDomain,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DomainsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('DomainsContext', () => {
|
||||
describe('Context Provider', () => {
|
||||
it('should provide default values when no provider is used', () => {
|
||||
const { getByTestId } = render(<TestComponent />);
|
||||
|
||||
expect(getByTestId('entity-data')).toHaveTextContent('null');
|
||||
expect(getByTestId('new-domain')).toHaveTextContent('null');
|
||||
expect(getByTestId('deleted-domain')).toHaveTextContent('null');
|
||||
expect(getByTestId('updated-domain')).toHaveTextContent('null');
|
||||
});
|
||||
|
||||
it('should allow setting and getting entityData', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvider>
|
||||
<TestComponent />
|
||||
</TestProvider>,
|
||||
);
|
||||
|
||||
expect(getByTestId('entity-data')).toHaveTextContent('null');
|
||||
|
||||
fireEvent.click(getByTestId('set-entity-data'));
|
||||
|
||||
expect(getByTestId('entity-data')).toHaveTextContent(
|
||||
JSON.stringify({ urn: 'test-urn', type: EntityType.Domain }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow setting and getting newDomain', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvider>
|
||||
<TestComponent />
|
||||
</TestProvider>,
|
||||
);
|
||||
|
||||
expect(getByTestId('new-domain')).toHaveTextContent('null');
|
||||
|
||||
fireEvent.click(getByTestId('set-new-domain'));
|
||||
|
||||
expect(getByTestId('new-domain')).toHaveTextContent('urn:li:domain:new');
|
||||
});
|
||||
|
||||
it('should allow setting and getting deletedDomain', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvider>
|
||||
<TestComponent />
|
||||
</TestProvider>,
|
||||
);
|
||||
|
||||
expect(getByTestId('deleted-domain')).toHaveTextContent('null');
|
||||
|
||||
fireEvent.click(getByTestId('set-deleted-domain'));
|
||||
|
||||
expect(getByTestId('deleted-domain')).toHaveTextContent('urn:li:domain:deleted');
|
||||
});
|
||||
|
||||
it('should allow setting and getting updatedDomain', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvider>
|
||||
<TestComponent />
|
||||
</TestProvider>,
|
||||
);
|
||||
|
||||
expect(getByTestId('updated-domain')).toHaveTextContent('null');
|
||||
|
||||
fireEvent.click(getByTestId('set-updated-domain'));
|
||||
|
||||
expect(getByTestId('updated-domain')).toHaveTextContent('urn:li:domain:updated');
|
||||
});
|
||||
|
||||
it('should allow clearing all context values', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvider>
|
||||
<TestComponent />
|
||||
</TestProvider>,
|
||||
);
|
||||
|
||||
// Set all values first
|
||||
fireEvent.click(getByTestId('set-entity-data'));
|
||||
fireEvent.click(getByTestId('set-new-domain'));
|
||||
fireEvent.click(getByTestId('set-deleted-domain'));
|
||||
fireEvent.click(getByTestId('set-updated-domain'));
|
||||
|
||||
// Verify they are set
|
||||
expect(getByTestId('entity-data')).not.toHaveTextContent('null');
|
||||
expect(getByTestId('new-domain')).not.toHaveTextContent('null');
|
||||
expect(getByTestId('deleted-domain')).not.toHaveTextContent('null');
|
||||
expect(getByTestId('updated-domain')).not.toHaveTextContent('null');
|
||||
|
||||
// Clear all
|
||||
fireEvent.click(getByTestId('clear-all'));
|
||||
|
||||
// Verify they are cleared
|
||||
expect(getByTestId('entity-data')).toHaveTextContent('null');
|
||||
expect(getByTestId('new-domain')).toHaveTextContent('null');
|
||||
expect(getByTestId('deleted-domain')).toHaveTextContent('null');
|
||||
expect(getByTestId('updated-domain')).toHaveTextContent('null');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDomainsContext hook', () => {
|
||||
it('should return all context values and setters', () => {
|
||||
let contextValues: any;
|
||||
|
||||
const TestComponent2 = () => {
|
||||
contextValues = useDomainsContext();
|
||||
return <div />;
|
||||
};
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestComponent2 />
|
||||
</TestProvider>,
|
||||
);
|
||||
|
||||
expect(contextValues).toHaveProperty('entityData');
|
||||
expect(contextValues).toHaveProperty('setEntityData');
|
||||
expect(contextValues).toHaveProperty('newDomain');
|
||||
expect(contextValues).toHaveProperty('setNewDomain');
|
||||
expect(contextValues).toHaveProperty('deletedDomain');
|
||||
expect(contextValues).toHaveProperty('setDeletedDomain');
|
||||
expect(contextValues).toHaveProperty('updatedDomain');
|
||||
expect(contextValues).toHaveProperty('setUpdatedDomain');
|
||||
|
||||
// Verify setters are functions
|
||||
expect(typeof contextValues.setEntityData).toBe('function');
|
||||
expect(typeof contextValues.setNewDomain).toBe('function');
|
||||
expect(typeof contextValues.setDeletedDomain).toBe('function');
|
||||
expect(typeof contextValues.setUpdatedDomain).toBe('function');
|
||||
});
|
||||
|
||||
it('should maintain separate state for different values', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvider>
|
||||
<TestComponent />
|
||||
</TestProvider>,
|
||||
);
|
||||
|
||||
// Set new domain
|
||||
fireEvent.click(getByTestId('set-new-domain'));
|
||||
expect(getByTestId('new-domain')).toHaveTextContent('urn:li:domain:new');
|
||||
expect(getByTestId('deleted-domain')).toHaveTextContent('null');
|
||||
expect(getByTestId('updated-domain')).toHaveTextContent('null');
|
||||
|
||||
// Set deleted domain
|
||||
fireEvent.click(getByTestId('set-deleted-domain'));
|
||||
expect(getByTestId('new-domain')).toHaveTextContent('urn:li:domain:new');
|
||||
expect(getByTestId('deleted-domain')).toHaveTextContent('urn:li:domain:deleted');
|
||||
expect(getByTestId('updated-domain')).toHaveTextContent('null');
|
||||
|
||||
// Set updated domain
|
||||
fireEvent.click(getByTestId('set-updated-domain'));
|
||||
expect(getByTestId('new-domain')).toHaveTextContent('urn:li:domain:new');
|
||||
expect(getByTestId('deleted-domain')).toHaveTextContent('urn:li:domain:deleted');
|
||||
expect(getByTestId('updated-domain')).toHaveTextContent('urn:li:domain:updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdatedDomain type', () => {
|
||||
it('should work with domains that have parentDomain property', () => {
|
||||
const domainWithParent = createMockDomain('urn:li:domain:child', 'Child Domain');
|
||||
domainWithParent.parentDomain = 'urn:li:domain:parent';
|
||||
|
||||
const TestComponentWithParent = () => {
|
||||
const { newDomain, setNewDomain } = useDomainsContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="domain-parent">{newDomain?.parentDomain || 'null'}</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="set-domain-with-parent"
|
||||
onClick={() => setNewDomain(domainWithParent)}
|
||||
>
|
||||
Set Domain With Parent
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProvider>
|
||||
<TestComponentWithParent />
|
||||
</TestProvider>,
|
||||
);
|
||||
|
||||
expect(getByTestId('domain-parent')).toHaveTextContent('null');
|
||||
|
||||
fireEvent.click(getByTestId('set-domain-with-parent'));
|
||||
|
||||
expect(getByTestId('domain-parent')).toHaveTextContent('urn:li:domain:parent');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,227 @@
|
||||
import { DOMAIN_COUNT, getDomainsScrollInput } from '@app/domainV2/useScrollDomains';
|
||||
import { ENTITY_NAME_FIELD } from '@app/searchV2/context/constants';
|
||||
|
||||
import { EntityType, FilterOperator, SortOrder } from '@types';
|
||||
|
||||
describe('getDomainsScrollInput', () => {
|
||||
describe('Root domains (parentDomain is null)', () => {
|
||||
it('should create correct input for root domains with no scrollId', () => {
|
||||
const result = getDomainsScrollInput(null, null);
|
||||
|
||||
expect(result).toEqual({
|
||||
input: {
|
||||
scrollId: null,
|
||||
query: '*',
|
||||
types: [EntityType.Domain],
|
||||
orFilters: [
|
||||
{
|
||||
and: [
|
||||
{
|
||||
field: 'parentDomain',
|
||||
condition: FilterOperator.Exists,
|
||||
negated: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
count: DOMAIN_COUNT,
|
||||
sortInput: {
|
||||
sortCriteria: [
|
||||
{
|
||||
field: ENTITY_NAME_FIELD,
|
||||
sortOrder: SortOrder.Ascending,
|
||||
},
|
||||
],
|
||||
},
|
||||
searchFlags: { skipCache: true },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create correct input for root domains with scrollId', () => {
|
||||
const scrollId = 'test-scroll-id-123';
|
||||
const result = getDomainsScrollInput(null, scrollId);
|
||||
|
||||
expect(result).toEqual({
|
||||
input: {
|
||||
scrollId,
|
||||
query: '*',
|
||||
types: [EntityType.Domain],
|
||||
orFilters: [
|
||||
{
|
||||
and: [
|
||||
{
|
||||
field: 'parentDomain',
|
||||
condition: FilterOperator.Exists,
|
||||
negated: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
count: DOMAIN_COUNT,
|
||||
sortInput: {
|
||||
sortCriteria: [
|
||||
{
|
||||
field: ENTITY_NAME_FIELD,
|
||||
sortOrder: SortOrder.Ascending,
|
||||
},
|
||||
],
|
||||
},
|
||||
searchFlags: { skipCache: true },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Child domains (parentDomain is provided)', () => {
|
||||
const parentDomainUrn = 'urn:li:domain:parent';
|
||||
|
||||
it('should create correct input for child domains with no scrollId', () => {
|
||||
const result = getDomainsScrollInput(parentDomainUrn, null);
|
||||
|
||||
expect(result).toEqual({
|
||||
input: {
|
||||
scrollId: null,
|
||||
query: '*',
|
||||
types: [EntityType.Domain],
|
||||
orFilters: [
|
||||
{
|
||||
and: [
|
||||
{
|
||||
field: 'parentDomain',
|
||||
values: [parentDomainUrn],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
count: DOMAIN_COUNT,
|
||||
sortInput: {
|
||||
sortCriteria: [
|
||||
{
|
||||
field: ENTITY_NAME_FIELD,
|
||||
sortOrder: SortOrder.Ascending,
|
||||
},
|
||||
],
|
||||
},
|
||||
searchFlags: { skipCache: true },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create correct input for child domains with scrollId', () => {
|
||||
const scrollId = 'child-scroll-id-456';
|
||||
const result = getDomainsScrollInput(parentDomainUrn, scrollId);
|
||||
|
||||
expect(result).toEqual({
|
||||
input: {
|
||||
scrollId,
|
||||
query: '*',
|
||||
types: [EntityType.Domain],
|
||||
orFilters: [
|
||||
{
|
||||
and: [
|
||||
{
|
||||
field: 'parentDomain',
|
||||
values: [parentDomainUrn],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
count: DOMAIN_COUNT,
|
||||
sortInput: {
|
||||
sortCriteria: [
|
||||
{
|
||||
field: ENTITY_NAME_FIELD,
|
||||
sortOrder: SortOrder.Ascending,
|
||||
},
|
||||
],
|
||||
},
|
||||
searchFlags: { skipCache: true },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty string parentDomain as no parent domain', () => {
|
||||
const result = getDomainsScrollInput('', null);
|
||||
|
||||
expect(result.input.orFilters).toEqual([
|
||||
{
|
||||
and: [
|
||||
{
|
||||
field: 'parentDomain',
|
||||
condition: FilterOperator.Exists,
|
||||
negated: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty string scrollId', () => {
|
||||
const result = getDomainsScrollInput(null, '');
|
||||
|
||||
expect(result.input.scrollId).toBe('');
|
||||
});
|
||||
|
||||
it('should handle undefined parentDomain same as null', () => {
|
||||
const resultNull = getDomainsScrollInput(null, null);
|
||||
const resultUndefined = getDomainsScrollInput(undefined as any, null);
|
||||
|
||||
expect(resultNull).toEqual(resultUndefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration consistency', () => {
|
||||
it('should always use the same query string', () => {
|
||||
const result1 = getDomainsScrollInput(null, null);
|
||||
const result2 = getDomainsScrollInput('urn:li:domain:parent', 'scroll-id');
|
||||
|
||||
expect(result1.input.query).toBe('*');
|
||||
expect(result2.input.query).toBe('*');
|
||||
});
|
||||
|
||||
it('should always include Domain entity type', () => {
|
||||
const result1 = getDomainsScrollInput(null, null);
|
||||
const result2 = getDomainsScrollInput('urn:li:domain:parent', 'scroll-id');
|
||||
|
||||
expect(result1.input.types).toEqual([EntityType.Domain]);
|
||||
expect(result2.input.types).toEqual([EntityType.Domain]);
|
||||
});
|
||||
|
||||
it('should always use the same count', () => {
|
||||
const result1 = getDomainsScrollInput(null, null);
|
||||
const result2 = getDomainsScrollInput('urn:li:domain:parent', 'scroll-id');
|
||||
|
||||
expect(result1.input.count).toBe(DOMAIN_COUNT);
|
||||
expect(result2.input.count).toBe(DOMAIN_COUNT);
|
||||
expect(result1.input.count).toBe(25); // Verify the constant value
|
||||
});
|
||||
|
||||
it('should always use ascending sort by name', () => {
|
||||
const result1 = getDomainsScrollInput(null, null);
|
||||
const result2 = getDomainsScrollInput('urn:li:domain:parent', 'scroll-id');
|
||||
|
||||
const expectedSortInput = {
|
||||
sortCriteria: [
|
||||
{
|
||||
field: ENTITY_NAME_FIELD,
|
||||
sortOrder: SortOrder.Ascending,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(result1.input.sortInput).toEqual(expectedSortInput);
|
||||
expect(result2.input.sortInput).toEqual(expectedSortInput);
|
||||
});
|
||||
|
||||
it('should always skip cache', () => {
|
||||
const result1 = getDomainsScrollInput(null, null);
|
||||
const result2 = getDomainsScrollInput('urn:li:domain:parent', 'scroll-id');
|
||||
|
||||
expect(result1.input.searchFlags).toEqual({ skipCache: true });
|
||||
expect(result2.input.searchFlags).toEqual({ skipCache: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,168 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
|
||||
import { DomainsContext } from '@app/domainV2/DomainsContext';
|
||||
import useScrollDomains from '@app/domainV2/useScrollDomains';
|
||||
|
||||
// Simple mock implementations
|
||||
const mockUseInView = vi.fn();
|
||||
const mockUseScrollAcrossEntitiesQuery = vi.fn();
|
||||
const mockUseManageDomains = vi.fn();
|
||||
|
||||
// Mock modules with simple return values
|
||||
vi.mock('react-intersection-observer', () => ({
|
||||
useInView: () => mockUseInView(),
|
||||
}));
|
||||
|
||||
vi.mock('@graphql/search.generated', () => ({
|
||||
useScrollAcrossEntitiesQuery: () => mockUseScrollAcrossEntitiesQuery(),
|
||||
}));
|
||||
|
||||
vi.mock('@app/domainV2/useManageDomains', () => ({
|
||||
default: () => mockUseManageDomains(),
|
||||
}));
|
||||
|
||||
const createWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<DomainsContext.Provider
|
||||
value={{
|
||||
entityData: null,
|
||||
setEntityData: vi.fn(),
|
||||
newDomain: null,
|
||||
setNewDomain: vi.fn(),
|
||||
deletedDomain: null,
|
||||
setDeletedDomain: vi.fn(),
|
||||
updatedDomain: null,
|
||||
setUpdatedDomain: vi.fn(),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DomainsContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
describe('Domains Integration Tests - Simple', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Set up default mock returns
|
||||
mockUseInView.mockReturnValue([vi.fn(), false]);
|
||||
mockUseScrollAcrossEntitiesQuery.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
mockUseManageDomains.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
describe('Basic functionality', () => {
|
||||
it('should initialize with empty data', () => {
|
||||
const { result } = renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.domains).toEqual([]);
|
||||
expect(result.current.hasInitialized).toBe(false);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
});
|
||||
|
||||
it('should return scrollRef function', () => {
|
||||
const { result } = renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.scrollRef).toBeDefined();
|
||||
expect(typeof result.current.scrollRef).toBe('function');
|
||||
});
|
||||
|
||||
it('should handle loading state', () => {
|
||||
mockUseScrollAcrossEntitiesQuery.mockReturnValue({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle error state', () => {
|
||||
const mockError = new Error('Test error');
|
||||
mockUseScrollAcrossEntitiesQuery.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: mockError,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe(mockError);
|
||||
});
|
||||
|
||||
it('should call useManageDomains hook', () => {
|
||||
renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(mockUseManageDomains).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call useScrollAcrossEntitiesQuery', () => {
|
||||
renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(mockUseScrollAcrossEntitiesQuery).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call useInView', () => {
|
||||
renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(mockUseInView).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hook integration', () => {
|
||||
it('should work with different parent domain values', () => {
|
||||
const { result: result1 } = renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
const { result: result2 } = renderHook(() => useScrollDomains({ parentDomain: 'urn:li:domain:test' }), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
// Both should initialize correctly
|
||||
expect(result1.current.domains).toEqual([]);
|
||||
expect(result2.current.domains).toEqual([]);
|
||||
});
|
||||
|
||||
it('should work with skip parameter', () => {
|
||||
mockUseScrollAcrossEntitiesQuery.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useScrollDomains({ skip: true }), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.domains).toEqual([]);
|
||||
expect(mockUseScrollAcrossEntitiesQuery).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,401 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
|
||||
import { DomainsContext, DomainsContextType, UpdatedDomain } from '@app/domainV2/DomainsContext';
|
||||
import useManageDomains from '@app/domainV2/useManageDomains';
|
||||
|
||||
import { ListDomainFragment } from '@graphql/domain.generated';
|
||||
import { EntityType } from '@types';
|
||||
|
||||
// Mock domain data for testing
|
||||
const createMockDomain = (urn: string, name: string, parentDomain?: string): UpdatedDomain => ({
|
||||
urn,
|
||||
id: urn,
|
||||
type: EntityType.Domain,
|
||||
properties: {
|
||||
name,
|
||||
description: `Description for ${name}`,
|
||||
},
|
||||
ownership: null,
|
||||
entities: null,
|
||||
children: { total: 0 },
|
||||
dataProducts: null,
|
||||
parentDomains: null,
|
||||
displayProperties: null,
|
||||
parentDomain,
|
||||
});
|
||||
|
||||
// Helper to create context wrapper for testing
|
||||
const createContextWrapper = (contextValue: Partial<DomainsContextType>) => {
|
||||
const defaultContextValue: DomainsContextType = {
|
||||
entityData: null,
|
||||
setEntityData: vi.fn(),
|
||||
newDomain: null,
|
||||
setNewDomain: vi.fn(),
|
||||
deletedDomain: null,
|
||||
setDeletedDomain: vi.fn(),
|
||||
updatedDomain: null,
|
||||
setUpdatedDomain: vi.fn(),
|
||||
...contextValue,
|
||||
};
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<DomainsContext.Provider value={defaultContextValue}>{children}</DomainsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useManageDomains', () => {
|
||||
let mockSetData: ReturnType<typeof vi.fn>;
|
||||
let mockSetDataUrnsSet: ReturnType<typeof vi.fn>;
|
||||
let mockSetNewDomain: ReturnType<typeof vi.fn>;
|
||||
let mockSetDeletedDomain: ReturnType<typeof vi.fn>;
|
||||
let mockSetUpdatedDomain: ReturnType<typeof vi.fn>;
|
||||
|
||||
const domain1 = createMockDomain('urn:li:domain:1', 'Domain 1');
|
||||
const domain2 = createMockDomain('urn:li:domain:2', 'Domain 2');
|
||||
|
||||
beforeEach(() => {
|
||||
mockSetData = vi.fn();
|
||||
mockSetDataUrnsSet = vi.fn();
|
||||
mockSetNewDomain = vi.fn();
|
||||
mockSetDeletedDomain = vi.fn();
|
||||
mockSetUpdatedDomain = vi.fn();
|
||||
});
|
||||
|
||||
describe('Adding new domain', () => {
|
||||
it('should add new domain to the list when parent domain matches', () => {
|
||||
const newDomain = createMockDomain('urn:li:domain:new', 'New Domain', 'urn:li:domain:1');
|
||||
const dataUrnsSet = new Set(['urn:li:domain:1', 'urn:li:domain:2']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
newDomain,
|
||||
setNewDomain: mockSetNewDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: 'urn:li:domain:1',
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(mockSetData).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(mockSetDataUrnsSet).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(mockSetNewDomain).toHaveBeenCalledWith(null);
|
||||
|
||||
// Test the data setter function
|
||||
const dataSetterFn = mockSetData.mock.calls[0][0];
|
||||
const newData = dataSetterFn([domain1, domain2]);
|
||||
expect(newData).toEqual([newDomain, domain1, domain2]);
|
||||
|
||||
// Test the dataUrnsSet setter function
|
||||
const urnsSetterFn = mockSetDataUrnsSet.mock.calls[0][0];
|
||||
const newUrnsSet = urnsSetterFn(dataUrnsSet);
|
||||
expect(newUrnsSet).toEqual(new Set(['urn:li:domain:1', 'urn:li:domain:2', 'urn:li:domain:new']));
|
||||
});
|
||||
|
||||
it('should not add new domain when parent domain does not match', () => {
|
||||
const newDomain = createMockDomain('urn:li:domain:new', 'New Domain', 'urn:li:domain:other');
|
||||
const dataUrnsSet = new Set(['urn:li:domain:1', 'urn:li:domain:2']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
newDomain,
|
||||
setNewDomain: mockSetNewDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: 'urn:li:domain:1',
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(mockSetData).not.toHaveBeenCalled();
|
||||
expect(mockSetDataUrnsSet).not.toHaveBeenCalled();
|
||||
expect(mockSetNewDomain).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should increase parent domain children count when new domain is added to a parent in the list', () => {
|
||||
const parentDomain = createMockDomain('urn:li:domain:parent', 'Parent Domain');
|
||||
parentDomain.children = { total: 5 };
|
||||
const newDomain = createMockDomain('urn:li:domain:new', 'New Domain', 'urn:li:domain:parent');
|
||||
const dataUrnsSet = new Set(['urn:li:domain:parent', 'urn:li:domain:2']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
newDomain,
|
||||
setNewDomain: mockSetNewDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: undefined, // We're at root level
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Should call setData twice - once for parent count update
|
||||
expect(mockSetData).toHaveBeenCalledTimes(1);
|
||||
|
||||
const dataSetterFn = mockSetData.mock.calls[0][0];
|
||||
const currentData = [parentDomain, domain2];
|
||||
const updatedData = dataSetterFn(currentData);
|
||||
|
||||
const updatedParent = updatedData.find((d: ListDomainFragment) => d.urn === 'urn:li:domain:parent');
|
||||
expect(updatedParent?.children?.total).toBe(6);
|
||||
});
|
||||
|
||||
it('should handle adding new root domain (no parent)', () => {
|
||||
const newRootDomain = createMockDomain('urn:li:domain:newroot', 'New Root Domain');
|
||||
// No parentDomain property for root domains
|
||||
delete newRootDomain.parentDomain;
|
||||
const dataUrnsSet = new Set(['urn:li:domain:1', 'urn:li:domain:2']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
newDomain: newRootDomain,
|
||||
setNewDomain: mockSetNewDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: undefined, // Root level
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(mockSetData).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(mockSetDataUrnsSet).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(mockSetNewDomain).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Deleting domain', () => {
|
||||
it('should remove domain from the list when domain exists in dataUrnsSet', () => {
|
||||
const deletedDomain = createMockDomain('urn:li:domain:2', 'Domain 2');
|
||||
const dataUrnsSet = new Set(['urn:li:domain:1', 'urn:li:domain:2']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
deletedDomain,
|
||||
setDeletedDomain: mockSetDeletedDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: undefined,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(mockSetData).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(mockSetDeletedDomain).toHaveBeenCalledWith(null);
|
||||
|
||||
// Test the data filter function
|
||||
const dataSetterFn = mockSetData.mock.calls[0][0];
|
||||
const filteredData = dataSetterFn([domain1, domain2]);
|
||||
expect(filteredData).toEqual([domain1]);
|
||||
});
|
||||
|
||||
it('should not remove domain when domain does not exist in dataUrnsSet', () => {
|
||||
const deletedDomain = createMockDomain('urn:li:domain:other', 'Other Domain');
|
||||
const dataUrnsSet = new Set(['urn:li:domain:1', 'urn:li:domain:2']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
deletedDomain,
|
||||
setDeletedDomain: mockSetDeletedDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: undefined,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(mockSetData).not.toHaveBeenCalled();
|
||||
expect(mockSetDeletedDomain).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should decrease parent domain children count when domain is deleted', () => {
|
||||
const parentDomain = createMockDomain('urn:li:domain:parent', 'Parent Domain');
|
||||
parentDomain.children = { total: 5 };
|
||||
const deletedDomain = createMockDomain('urn:li:domain:child', 'Child Domain', 'urn:li:domain:parent');
|
||||
const dataUrnsSet = new Set(['urn:li:domain:parent', 'urn:li:domain:child']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
deletedDomain,
|
||||
setDeletedDomain: mockSetDeletedDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: undefined,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(mockSetData).toHaveBeenCalledTimes(2); // Once for deletion, once for parent count update
|
||||
|
||||
// Test parent count update
|
||||
const parentUpdateFn = mockSetData.mock.calls[1][0];
|
||||
const currentData = [parentDomain, deletedDomain];
|
||||
const updatedData = parentUpdateFn(currentData);
|
||||
|
||||
const updatedParent = updatedData.find((d: ListDomainFragment) => d.urn === 'urn:li:domain:parent');
|
||||
expect(updatedParent?.children?.total).toBe(4);
|
||||
});
|
||||
|
||||
it('should not decrease parent count below 0', () => {
|
||||
const parentDomain = createMockDomain('urn:li:domain:parent', 'Parent Domain');
|
||||
parentDomain.children = { total: 0 };
|
||||
const deletedDomain = createMockDomain('urn:li:domain:child', 'Child Domain', 'urn:li:domain:parent');
|
||||
const dataUrnsSet = new Set(['urn:li:domain:parent', 'urn:li:domain:child']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
deletedDomain,
|
||||
setDeletedDomain: mockSetDeletedDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: undefined,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const parentUpdateFn = mockSetData.mock.calls[1][0];
|
||||
const currentData = [parentDomain];
|
||||
const updatedData = parentUpdateFn(currentData);
|
||||
|
||||
const updatedParent = updatedData.find((d: ListDomainFragment) => d.urn === 'urn:li:domain:parent');
|
||||
expect(updatedParent?.children?.total).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Updating domain', () => {
|
||||
it('should update domain in the list when domain exists in dataUrnsSet', () => {
|
||||
const updatedDomain = createMockDomain('urn:li:domain:2', 'Updated Domain 2');
|
||||
updatedDomain.properties!.description = 'Updated description';
|
||||
const dataUrnsSet = new Set(['urn:li:domain:1', 'urn:li:domain:2']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
updatedDomain,
|
||||
setUpdatedDomain: mockSetUpdatedDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: undefined,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(mockSetData).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(mockSetUpdatedDomain).toHaveBeenCalledWith(null);
|
||||
|
||||
// Test the data update function
|
||||
const dataSetterFn = mockSetData.mock.calls[0][0];
|
||||
const updatedData = dataSetterFn([domain1, domain2]);
|
||||
|
||||
expect(updatedData[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
urn: 'urn:li:domain:2',
|
||||
properties: expect.objectContaining({
|
||||
name: 'Updated Domain 2',
|
||||
description: 'Updated description',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not update domain when domain does not exist in dataUrnsSet', () => {
|
||||
const updatedDomain = createMockDomain('urn:li:domain:other', 'Other Domain');
|
||||
const dataUrnsSet = new Set(['urn:li:domain:1', 'urn:li:domain:2']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
updatedDomain,
|
||||
setUpdatedDomain: mockSetUpdatedDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: undefined,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(mockSetData).not.toHaveBeenCalled();
|
||||
expect(mockSetUpdatedDomain).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Effect dependencies', () => {
|
||||
it('should react to changes in context values', () => {
|
||||
const newDomain = createMockDomain('urn:li:domain:new', 'New Domain');
|
||||
const dataUrnsSet = new Set(['urn:li:domain:1']);
|
||||
|
||||
const wrapper = createContextWrapper({
|
||||
newDomain,
|
||||
setNewDomain: mockSetNewDomain,
|
||||
deletedDomain: null,
|
||||
setDeletedDomain: mockSetDeletedDomain,
|
||||
updatedDomain: null,
|
||||
setUpdatedDomain: mockSetUpdatedDomain,
|
||||
});
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useManageDomains({
|
||||
dataUrnsSet,
|
||||
setDataUrnsSet: mockSetDataUrnsSet,
|
||||
setData: mockSetData,
|
||||
parentDomain: undefined,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Should be called because newDomain is provided and parentDomain matches (both undefined)
|
||||
expect(mockSetData).toHaveBeenCalled();
|
||||
expect(mockSetDataUrnsSet).toHaveBeenCalled();
|
||||
expect(mockSetNewDomain).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,118 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
|
||||
import { DomainsContext } from '@app/domainV2/DomainsContext';
|
||||
import useScrollDomains from '@app/domainV2/useScrollDomains';
|
||||
|
||||
// Simple mock implementations
|
||||
const mockUseInView = vi.fn();
|
||||
const mockUseScrollAcrossEntitiesQuery = vi.fn();
|
||||
const mockUseManageDomains = vi.fn();
|
||||
|
||||
// Mock modules with simple return values
|
||||
vi.mock('react-intersection-observer', () => ({
|
||||
useInView: () => mockUseInView(),
|
||||
}));
|
||||
|
||||
vi.mock('@graphql/search.generated', () => ({
|
||||
useScrollAcrossEntitiesQuery: () => mockUseScrollAcrossEntitiesQuery(),
|
||||
}));
|
||||
|
||||
vi.mock('@app/domainV2/useManageDomains', () => ({
|
||||
default: () => mockUseManageDomains(),
|
||||
}));
|
||||
|
||||
const createWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<DomainsContext.Provider
|
||||
value={{
|
||||
entityData: null,
|
||||
setEntityData: vi.fn(),
|
||||
newDomain: null,
|
||||
setNewDomain: vi.fn(),
|
||||
deletedDomain: null,
|
||||
setDeletedDomain: vi.fn(),
|
||||
updatedDomain: null,
|
||||
setUpdatedDomain: vi.fn(),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DomainsContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
describe('useScrollDomains - Simplified Tests', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Set up default mock returns
|
||||
mockUseInView.mockReturnValue([vi.fn(), false]);
|
||||
mockUseScrollAcrossEntitiesQuery.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
mockUseManageDomains.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it('should initialize with empty data', () => {
|
||||
const { result } = renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.domains).toEqual([]);
|
||||
expect(result.current.hasInitialized).toBe(false);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
});
|
||||
|
||||
it('should return scrollRef function', () => {
|
||||
const { result } = renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.scrollRef).toBeDefined();
|
||||
expect(typeof result.current.scrollRef).toBe('function');
|
||||
});
|
||||
|
||||
it('should handle loading state', () => {
|
||||
mockUseScrollAcrossEntitiesQuery.mockReturnValue({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle error state', () => {
|
||||
const mockError = new Error('Test error');
|
||||
mockUseScrollAcrossEntitiesQuery.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: mockError,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe(mockError);
|
||||
});
|
||||
|
||||
it('should call useManageDomains hook', () => {
|
||||
renderHook(() => useScrollDomains({}), {
|
||||
wrapper: createWrapper,
|
||||
});
|
||||
|
||||
expect(mockUseManageDomains).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -1,10 +1,8 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Button, Tooltip } from '@components';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import CreateDomainModal from '@app/domainV2/CreateDomainModal';
|
||||
import { updateListDomainsCache } from '@app/domainV2/utils';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 20px;
|
||||
@ -31,7 +29,6 @@ const StyledButton = styled(Button)`
|
||||
|
||||
export default function DomainsSidebarHeader() {
|
||||
const [isCreatingDomain, setIsCreatingDomain] = useState(false);
|
||||
const client = useApolloClient();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
@ -45,14 +42,7 @@ export default function DomainsSidebarHeader() {
|
||||
onClick={() => setIsCreatingDomain(true)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{isCreatingDomain && (
|
||||
<CreateDomainModal
|
||||
onClose={() => setIsCreatingDomain(false)}
|
||||
onCreate={(urn, id, name, description, parentDomain) => {
|
||||
updateListDomainsCache(client, urn, id, name, description, parentDomain);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isCreatingDomain && <CreateDomainModal onClose={() => setIsCreatingDomain(false)} />}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
@ -6,7 +5,6 @@ import styled from 'styled-components/macro';
|
||||
import CreateDomainModal from '@app/domainV2/CreateDomainModal';
|
||||
import { useDomainsContext as useDomainsContextV2 } from '@app/domainV2/DomainsContext';
|
||||
import RootDomains from '@app/domainV2/nestedDomains/RootDomains';
|
||||
import { updateListDomainsCache } from '@app/domainV2/utils';
|
||||
import { OnboardingTour } from '@app/onboarding/OnboardingTour';
|
||||
import { DOMAINS_CREATE_DOMAIN_ID, DOMAINS_INTRO_ID } from '@app/onboarding/config/DomainsOnboardingConfig';
|
||||
import { Button } from '@src/alchemy-components';
|
||||
@ -35,7 +33,6 @@ const Header = styled.div`
|
||||
export default function ManageDomainsPageV2() {
|
||||
const { setEntityData } = useDomainsContextV2();
|
||||
const [isCreatingDomain, setIsCreatingDomain] = useState(false);
|
||||
const client = useApolloClient();
|
||||
const isShowNavBarRedesign = useShowNavBarRedesign();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
@ -69,14 +66,7 @@ export default function ManageDomainsPageV2() {
|
||||
</Button>
|
||||
</Header>
|
||||
<RootDomains setIsCreatingDomain={setIsCreatingDomain} />
|
||||
{isCreatingDomain && (
|
||||
<CreateDomainModal
|
||||
onClose={() => setIsCreatingDomain(false)}
|
||||
onCreate={(urn, id, name, description, parentDomain) =>
|
||||
updateListDomainsCache(client, urn, id, name, description, parentDomain)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isCreatingDomain && <CreateDomainModal onClose={() => setIsCreatingDomain(false)} />}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ export default function ManageDomainsSidebarV2({ isEntityProfile }: Props) {
|
||||
<StyledSidebar>
|
||||
<DomainSearch isCollapsed={isClosed} unhideSidebar={unhideSidebar} />
|
||||
<ThinDivider />
|
||||
<DomainNavigator isCollapsed={isClosed} unhideSidebar={unhideSidebar} />
|
||||
<DomainNavigator isCollapsed={isClosed} unhideSidebar={unhideSidebar} variant="sidebar" />
|
||||
</StyledSidebar>
|
||||
</StyledEntitySidebarContainer>
|
||||
);
|
||||
|
||||
@ -4,7 +4,8 @@ import styled from 'styled-components';
|
||||
|
||||
import EmptyDomainDescription from '@app/domainV2/EmptyDomainDescription';
|
||||
import EmptyDomainsSection from '@app/domainV2/EmptyDomainsSection';
|
||||
import useListDomains from '@app/domainV2/useListDomains';
|
||||
import useScrollDomains from '@app/domainV2/useScrollDomains';
|
||||
import Loading from '@app/shared/Loading';
|
||||
import { Message } from '@app/shared/Message';
|
||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||
|
||||
@ -28,18 +29,21 @@ const ResultWrapper = styled.div`
|
||||
border: 1px solid #ebecf0;
|
||||
`;
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
setIsCreatingDomain: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
export default function RootDomains({ setIsCreatingDomain }: Props) {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const { loading, error, data, sortedDomains } = useListDomains({});
|
||||
const { domains, hasInitialized, loading, error, scrollRef } = useScrollDomains({});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!data && loading && <Message type="loading" content="Loading domains..." />}
|
||||
{error && <Message type="error" content="Failed to load domains. An unexpected error occurred." />}
|
||||
{!loading && (!data || !data?.listDomains?.domains?.length) && (
|
||||
{hasInitialized && domains.length === 0 && (
|
||||
<EmptyDomainsSection
|
||||
icon={<ReadOutlined />}
|
||||
title="Organize your data"
|
||||
@ -48,11 +52,17 @@ export default function RootDomains({ setIsCreatingDomain }: Props) {
|
||||
/>
|
||||
)}
|
||||
<DomainsWrapper>
|
||||
{sortedDomains?.map((domain) => (
|
||||
{domains?.map((domain) => (
|
||||
<ResultWrapper key={domain.urn}>
|
||||
{entityRegistry.renderSearchResult(EntityType.Domain, { entity: domain, matchedFields: [] })}
|
||||
</ResultWrapper>
|
||||
))}
|
||||
{loading && (
|
||||
<LoadingWrapper>
|
||||
<Loading height={24} marginTop={0} />
|
||||
</LoadingWrapper>
|
||||
)}
|
||||
{domains.length > 0 && <div ref={scrollRef} />}
|
||||
</DomainsWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -3,8 +3,10 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import DomainNode from '@app/domainV2/nestedDomains/domainNavigator/DomainNode';
|
||||
import useListDomains from '@app/domainV2/useListDomains';
|
||||
import { DomainNavigatorVariant } from '@app/domainV2/nestedDomains/types';
|
||||
import useScrollDomains from '@app/domainV2/useScrollDomains';
|
||||
import { ANTD_GRAY } from '@app/entity/shared/constants';
|
||||
import Loading from '@app/shared/Loading';
|
||||
|
||||
import { Domain } from '@types';
|
||||
|
||||
@ -14,39 +16,55 @@ const NavigatorWrapper = styled.div`
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
domainUrnToHide?: string;
|
||||
selectDomainOverride?: (domain: Domain) => void;
|
||||
isCollapsed: boolean;
|
||||
isCollapsed?: boolean;
|
||||
unhideSidebar?: () => void;
|
||||
variant?: DomainNavigatorVariant;
|
||||
}
|
||||
|
||||
export default function DomainNavigator({ domainUrnToHide, isCollapsed, selectDomainOverride, unhideSidebar }: Props) {
|
||||
const { sortedDomains, error, loading } = useListDomains({});
|
||||
const noDomainsFound: boolean = !sortedDomains || sortedDomains.length === 0;
|
||||
export default function DomainNavigator({
|
||||
domainUrnToHide,
|
||||
isCollapsed,
|
||||
selectDomainOverride,
|
||||
unhideSidebar,
|
||||
variant = 'select',
|
||||
}: Props) {
|
||||
const { domains, hasInitialized, loading, error, scrollRef } = useScrollDomains({});
|
||||
|
||||
return (
|
||||
<NavigatorWrapper>
|
||||
{error && <Alert message="Loading Domains failed." showIcon type="error" />}
|
||||
{!loading && noDomainsFound && (
|
||||
{hasInitialized && domains.length === 0 && (
|
||||
<Empty
|
||||
description="No Domains Found"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
style={{ color: ANTD_GRAY[7] }}
|
||||
/>
|
||||
)}
|
||||
{!noDomainsFound &&
|
||||
sortedDomains?.map((domain) => (
|
||||
<DomainNode
|
||||
key={domain.urn}
|
||||
domain={domain as Domain}
|
||||
numDomainChildren={domain.children?.total || 0}
|
||||
domainUrnToHide={domainUrnToHide}
|
||||
selectDomainOverride={selectDomainOverride}
|
||||
isCollapsed={isCollapsed}
|
||||
unhideSidebar={unhideSidebar}
|
||||
/>
|
||||
))}
|
||||
{domains?.map((domain) => (
|
||||
<DomainNode
|
||||
key={domain.urn}
|
||||
domain={domain as Domain}
|
||||
numDomainChildren={domain.children?.total || 0}
|
||||
domainUrnToHide={domainUrnToHide}
|
||||
selectDomainOverride={selectDomainOverride}
|
||||
isCollapsed={isCollapsed}
|
||||
unhideSidebar={unhideSidebar}
|
||||
variant={variant}
|
||||
/>
|
||||
))}
|
||||
{loading && (
|
||||
<LoadingWrapper>
|
||||
<Loading height={24} marginTop={0} />
|
||||
</LoadingWrapper>
|
||||
)}
|
||||
{domains.length > 0 && <div ref={scrollRef} />}
|
||||
</NavigatorWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { Tooltip, colors } from '@components';
|
||||
import { Pill, Tooltip } from '@components';
|
||||
import { Typography } from 'antd';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useDomainsContext as useDomainsContextV2 } from '@app/domainV2/DomainsContext';
|
||||
import useListDomains from '@app/domainV2/useListDomains';
|
||||
import { DomainNavigatorVariant } from '@app/domainV2/nestedDomains/types';
|
||||
import useScrollDomains from '@app/domainV2/useScrollDomains';
|
||||
import { REDESIGN_COLORS } from '@app/entityV2/shared/constants';
|
||||
import { DomainColoredIcon } from '@app/entityV2/shared/links/DomainColoredIcon';
|
||||
import Loading from '@app/shared/Loading';
|
||||
import { BodyContainer, BodyGridExpander } from '@app/shared/components';
|
||||
import useToggle from '@app/shared/useToggle';
|
||||
import { RotatingTriangle } from '@app/sharedV2/sidebar/components';
|
||||
@ -15,26 +17,11 @@ import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||
|
||||
import { Domain } from '@types';
|
||||
|
||||
const Count = styled.div`
|
||||
color: ${colors.gray[1700]};
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
margin-left: 8px;
|
||||
border-radius: 20px;
|
||||
background-color: ${colors.gray[100]};
|
||||
height: 22px;
|
||||
min-width: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const NameWrapper = styled(Typography.Text)<{ $isSelected: boolean; $addLeftPadding: boolean }>`
|
||||
flex: 1;
|
||||
padding: 2px;
|
||||
${(props) => props.$isSelected && `color: ${props.theme.styles['primary-color']};`}
|
||||
${(props) => props.$addLeftPadding && 'padding-left: 20px;'}
|
||||
${(props) => props.$addLeftPadding && 'padding-left: 22px;'}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
@ -69,12 +56,13 @@ const ButtonWrapper = styled.span<{ $addLeftPadding: boolean; $isSelected: boole
|
||||
}
|
||||
`;
|
||||
|
||||
const RowWrapper = styled.div<{ $isSelected: boolean; isOpen?: boolean }>`
|
||||
const RowWrapper = styled.div<{ $isSelected: boolean; isOpen?: boolean; $variant: DomainNavigatorVariant }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid ${REDESIGN_COLORS.COLD_GREY_TEXT_BLUE_1};
|
||||
padding: 12px;
|
||||
border-bottom: ${({ $variant }) =>
|
||||
$variant === 'select' ? 'none' : `1px solid ${REDESIGN_COLORS.COLD_GREY_TEXT_BLUE_1}`};
|
||||
padding: ${({ $variant }) => ($variant === 'select' ? '6px' : '12px')};
|
||||
${(props) => props.isOpen && `background-color: ${REDESIGN_COLORS.SECTION_BACKGROUND};`}
|
||||
${(props) => props.$isSelected && `background-color: ${REDESIGN_COLORS.LIGHT_TEXT_DARK_BACKGROUND};`}
|
||||
&:hover {
|
||||
@ -109,6 +97,10 @@ const Text = styled.div`
|
||||
width: 80%;
|
||||
`;
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
domain: Domain;
|
||||
numDomainChildren: number;
|
||||
@ -117,6 +109,7 @@ interface Props {
|
||||
selectDomainOverride?: (domain: Domain) => void;
|
||||
unhideSidebar?: () => void;
|
||||
$paddingLeft?: number;
|
||||
variant?: DomainNavigatorVariant;
|
||||
}
|
||||
|
||||
export default function DomainNode({
|
||||
@ -127,6 +120,7 @@ export default function DomainNode({
|
||||
selectDomainOverride,
|
||||
unhideSidebar,
|
||||
$paddingLeft = 0,
|
||||
variant = 'select',
|
||||
}: Props) {
|
||||
const shouldHideDomain = domainUrnToHide === domain.urn;
|
||||
const history = useHistory();
|
||||
@ -136,7 +130,10 @@ export default function DomainNode({
|
||||
initialValue: false,
|
||||
closeDelay: 250,
|
||||
});
|
||||
const { sortedDomains } = useListDomains({ parentDomain: domain.urn, skip: !isOpen || shouldHideDomain });
|
||||
const { domains, loading, scrollRef } = useScrollDomains({
|
||||
parentDomain: domain.urn,
|
||||
skip: !isOpen || shouldHideDomain,
|
||||
});
|
||||
const isOnEntityPage = entityData && entityData.urn === domain.urn;
|
||||
const displayName = entityRegistry.getDisplayName(domain.type, isOnEntityPage ? entityData : domain);
|
||||
const isInSelectMode = !!selectDomainOverride;
|
||||
@ -169,8 +166,7 @@ export default function DomainNode({
|
||||
|
||||
if (shouldHideDomain) return null;
|
||||
|
||||
const finalNumChildren = sortedDomains?.length ?? numDomainChildren;
|
||||
const hasDomainChildren = !!finalNumChildren;
|
||||
const hasDomainChildren = !!numDomainChildren;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -178,6 +174,7 @@ export default function DomainNode({
|
||||
data-testid="domain-list-item"
|
||||
$isSelected={isDomainNodeSelected && !isCollapsed}
|
||||
isOpen={isOpen && !isClosing}
|
||||
$variant={variant}
|
||||
>
|
||||
{!isCollapsed && hasDomainChildren && (
|
||||
<ButtonWrapper
|
||||
@ -210,23 +207,34 @@ export default function DomainNode({
|
||||
{!isCollapsed && displayName}
|
||||
</DisplayName>
|
||||
</Text>
|
||||
{!isCollapsed && hasDomainChildren && <Count>{finalNumChildren}</Count>}
|
||||
{!isCollapsed && hasDomainChildren && <Pill label={`${numDomainChildren}`} size="sm" />}
|
||||
</NameWrapper>
|
||||
</Tooltip>
|
||||
</RowWrapper>
|
||||
<StyledExpander isOpen={isOpen && !isClosing} paddingLeft={paddingLeft}>
|
||||
<BodyContainer style={{ width: '100%' }}>
|
||||
{sortedDomains?.map((childDomain) => (
|
||||
<DomainNode
|
||||
key={domain.urn}
|
||||
domain={childDomain as Domain}
|
||||
numDomainChildren={childDomain.children?.total || 0}
|
||||
domainUrnToHide={domainUrnToHide}
|
||||
selectDomainOverride={selectDomainOverride}
|
||||
unhideSidebar={unhideSidebar}
|
||||
$paddingLeft={paddingLeft}
|
||||
/>
|
||||
))}
|
||||
{isOpen && (
|
||||
<>
|
||||
{domains?.map((childDomain) => (
|
||||
<DomainNode
|
||||
key={domain.urn}
|
||||
domain={childDomain as Domain}
|
||||
numDomainChildren={childDomain.children?.total || 0}
|
||||
domainUrnToHide={domainUrnToHide}
|
||||
selectDomainOverride={selectDomainOverride}
|
||||
unhideSidebar={unhideSidebar}
|
||||
$paddingLeft={paddingLeft}
|
||||
variant={variant}
|
||||
/>
|
||||
))}
|
||||
{loading && (
|
||||
<LoadingWrapper>
|
||||
<Loading height={16} marginTop={0} />
|
||||
</LoadingWrapper>
|
||||
)}
|
||||
{domains.length > 0 && <div ref={scrollRef} />}
|
||||
</>
|
||||
)}
|
||||
</BodyContainer>
|
||||
</StyledExpander>
|
||||
</>
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export type DomainNavigatorVariant = 'sidebar' | 'select';
|
||||
@ -1,28 +0,0 @@
|
||||
import { useSortedDomains } from '@app/domainV2/utils';
|
||||
|
||||
import { useListDomainsQuery } from '@graphql/domain.generated';
|
||||
|
||||
interface Props {
|
||||
parentDomain?: string;
|
||||
skip?: boolean;
|
||||
sortBy?: 'displayName';
|
||||
}
|
||||
|
||||
export default function useListDomains({ parentDomain, skip, sortBy = 'displayName' }: Props) {
|
||||
const { data, error, loading, refetch } = useListDomainsQuery({
|
||||
skip,
|
||||
variables: {
|
||||
input: {
|
||||
start: 0,
|
||||
count: 1000, // don't paginate the home page, get all root level domains
|
||||
parentDomain,
|
||||
},
|
||||
},
|
||||
fetchPolicy: 'network-only', // always use network request first to populate cache
|
||||
nextFetchPolicy: 'cache-first', // then use cache after that so we can manipulate it
|
||||
});
|
||||
|
||||
const sortedDomains = useSortedDomains(data?.listDomains?.domains, sortBy);
|
||||
|
||||
return { data, sortedDomains, error, loading, refetch };
|
||||
}
|
||||
75
datahub-web-react/src/app/domainV2/useManageDomains.ts
Normal file
75
datahub-web-react/src/app/domainV2/useManageDomains.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useDomainsContext } from '@app/domainV2/DomainsContext';
|
||||
|
||||
import { ListDomainFragment } from '@graphql/domain.generated';
|
||||
|
||||
interface Props {
|
||||
dataUrnsSet: Set<string>;
|
||||
setDataUrnsSet: React.Dispatch<React.SetStateAction<Set<string>>>;
|
||||
setData: React.Dispatch<React.SetStateAction<ListDomainFragment[]>>;
|
||||
parentDomain?: string;
|
||||
}
|
||||
|
||||
export default function useManageDomains({ dataUrnsSet, setDataUrnsSet, setData, parentDomain }: Props) {
|
||||
const { newDomain, setNewDomain, deletedDomain, setDeletedDomain, updatedDomain, setUpdatedDomain } =
|
||||
useDomainsContext();
|
||||
|
||||
// Adding new domain
|
||||
useEffect(() => {
|
||||
if (newDomain && newDomain.parentDomain === parentDomain) {
|
||||
setData((prevData) => [newDomain, ...prevData]);
|
||||
setDataUrnsSet((currSet) => new Set([...currSet, newDomain.urn]));
|
||||
setNewDomain(null);
|
||||
}
|
||||
|
||||
// adding a new domain should increase the count of its parent
|
||||
const newDomainParentUrn = newDomain?.parentDomain;
|
||||
if (newDomainParentUrn && dataUrnsSet.has(newDomainParentUrn)) {
|
||||
setData((currData) =>
|
||||
currData.map((d) => {
|
||||
if (d.urn === newDomainParentUrn) {
|
||||
return { ...d, children: { total: (d.children?.total || 0) + 1 } };
|
||||
}
|
||||
return d;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [newDomain, dataUrnsSet, parentDomain, setData, setDataUrnsSet, setNewDomain]);
|
||||
|
||||
// Deleting domain
|
||||
useEffect(() => {
|
||||
if (deletedDomain && dataUrnsSet.has(deletedDomain.urn)) {
|
||||
setData((prevData) => prevData.filter((d) => d.urn !== deletedDomain.urn));
|
||||
setDeletedDomain(null);
|
||||
}
|
||||
|
||||
// deleting a new domain should decrease the count of its parent
|
||||
const deletedDomainParentUrn = deletedDomain?.parentDomain;
|
||||
if (deletedDomainParentUrn && dataUrnsSet.has(deletedDomainParentUrn)) {
|
||||
setData((currData) =>
|
||||
currData.map((d) => {
|
||||
if (d.urn === deletedDomainParentUrn) {
|
||||
return { ...d, children: { total: Math.max((d.children?.total || 0) - 1, 0) } };
|
||||
}
|
||||
return d;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [deletedDomain, dataUrnsSet, setData, setDeletedDomain]);
|
||||
|
||||
// Updating domain
|
||||
useEffect(() => {
|
||||
if (updatedDomain && dataUrnsSet.has(updatedDomain.urn)) {
|
||||
setData((prevData) =>
|
||||
prevData.map((d) => {
|
||||
if (d.urn === updatedDomain.urn) {
|
||||
return { ...d, ...updatedDomain };
|
||||
}
|
||||
return d;
|
||||
}),
|
||||
);
|
||||
setUpdatedDomain(null);
|
||||
}
|
||||
}, [updatedDomain, dataUrnsSet, setData, setUpdatedDomain]);
|
||||
}
|
||||
96
datahub-web-react/src/app/domainV2/useScrollDomains.ts
Normal file
96
datahub-web-react/src/app/domainV2/useScrollDomains.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
import useManageDomains from '@app/domainV2/useManageDomains';
|
||||
import { ENTITY_NAME_FIELD } from '@app/searchV2/context/constants';
|
||||
|
||||
import { ListDomainFragment } from '@graphql/domain.generated';
|
||||
import { useScrollAcrossEntitiesQuery } from '@graphql/search.generated';
|
||||
import { EntityType, FilterOperator, SortOrder } from '@types';
|
||||
|
||||
export const DOMAIN_COUNT = 25;
|
||||
|
||||
export function getDomainsScrollInput(parentDomain: string | null, scrollId: string | null) {
|
||||
return {
|
||||
input: {
|
||||
scrollId,
|
||||
query: '*',
|
||||
types: [EntityType.Domain],
|
||||
orFilters: parentDomain
|
||||
? [{ and: [{ field: 'parentDomain', values: [parentDomain] }] }]
|
||||
: [{ and: [{ field: 'parentDomain', condition: FilterOperator.Exists, negated: true }] }],
|
||||
count: DOMAIN_COUNT,
|
||||
sortInput: {
|
||||
sortCriteria: [{ field: ENTITY_NAME_FIELD, sortOrder: SortOrder.Ascending }],
|
||||
},
|
||||
searchFlags: { skipCache: true },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
parentDomain?: string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export default function useScrollDomains({ parentDomain, skip }: Props) {
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
const [data, setData] = useState<ListDomainFragment[]>([]);
|
||||
const [dataUrnsSet, setDataUrnsSet] = useState<Set<string>>(new Set());
|
||||
const [scrollId, setScrollId] = useState<string | null>(null);
|
||||
const {
|
||||
data: scrollData,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
} = useScrollAcrossEntitiesQuery({
|
||||
variables: {
|
||||
...getDomainsScrollInput(parentDomain || null, scrollId),
|
||||
},
|
||||
skip,
|
||||
notifyOnNetworkStatusChange: true,
|
||||
fetchPolicy: 'cache-and-network', // to do check this still good
|
||||
});
|
||||
|
||||
// Manage CRUD of domains on domains page
|
||||
useManageDomains({ dataUrnsSet, setDataUrnsSet, setData, parentDomain });
|
||||
|
||||
// Handle initial data and updates from scroll
|
||||
useEffect(() => {
|
||||
if (scrollData?.scrollAcrossEntities?.searchResults) {
|
||||
const newResults = (scrollData.scrollAcrossEntities.searchResults
|
||||
.filter((r) => !dataUrnsSet.has(r.entity.urn))
|
||||
.map((r) => r.entity)
|
||||
.filter((e) => e.type === EntityType.Domain) || []) as ListDomainFragment[];
|
||||
|
||||
if (newResults.length > 0) {
|
||||
setData((currData) => [...currData, ...newResults]);
|
||||
setDataUrnsSet((currSet) => {
|
||||
const newSet = new Set(currSet);
|
||||
newResults.forEach((r) => newSet.add(r.urn));
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
setHasInitialized(true);
|
||||
}
|
||||
}, [scrollData, dataUrnsSet]);
|
||||
|
||||
const nextScrollId = scrollData?.scrollAcrossEntities?.nextScrollId;
|
||||
|
||||
const [scrollRef, inView] = useInView({ triggerOnce: false });
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && nextScrollId && scrollId !== nextScrollId && inView) {
|
||||
setScrollId(nextScrollId);
|
||||
}
|
||||
}, [inView, nextScrollId, scrollId, loading]);
|
||||
|
||||
return {
|
||||
domains: data,
|
||||
hasInitialized,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
scrollRef,
|
||||
};
|
||||
}
|
||||
@ -2,8 +2,8 @@ import { CloseCircleFilled } from '@ant-design/icons';
|
||||
import { Empty, Select } from 'antd';
|
||||
import React, { MouseEvent } from 'react';
|
||||
|
||||
import DomainNavigator from '@app/domain/nestedDomains/domainNavigator/DomainNavigator';
|
||||
import domainAutocompleteOptions from '@app/domainV2/DomainAutocompleteOptions';
|
||||
import DomainNavigator from '@app/domainV2/nestedDomains/domainNavigator/DomainNavigator';
|
||||
import useParentSelector from '@app/entityV2/shared/EntityDropdown/useParentSelector';
|
||||
import { ANTD_GRAY } from '@app/entityV2/shared/constants';
|
||||
import ClickOutside from '@app/shared/ClickOutside';
|
||||
@ -102,6 +102,7 @@ export default function DomainParentSelect({ selectedParentUrn, setSelectedParen
|
||||
<DomainNavigator
|
||||
domainUrnToHide={isMoving ? domainUrn : undefined}
|
||||
selectDomainOverride={selectDomain}
|
||||
isCollapsed={false}
|
||||
/>
|
||||
</BrowserWrapper>
|
||||
</ClickOutside>
|
||||
|
||||
@ -52,7 +52,7 @@ function MoveDomainModal(props: Props) {
|
||||
.then(() => {
|
||||
message.loading({ content: 'Updating...', duration: 2 });
|
||||
const newParentToUpdate = selectedParentUrn || undefined;
|
||||
handleMoveDomainComplete(domainUrn, newParentToUpdate);
|
||||
handleMoveDomainComplete(newParentToUpdate);
|
||||
setTimeout(() => {
|
||||
message.success({
|
||||
content: `Moved ${entityRegistry.getEntityName(EntityType.Domain)}!`,
|
||||
|
||||
@ -1,27 +1,25 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { useDomainsContext } from '@app/domain/DomainsContext';
|
||||
import { removeFromListDomainsCache } from '@app/domain/utils';
|
||||
import { UpdatedDomain, useDomainsContext } from '@app/domainV2/DomainsContext';
|
||||
import { GenericEntityProperties } from '@app/entity/shared/types';
|
||||
|
||||
import { EntityType } from '@types';
|
||||
|
||||
interface DeleteDomainProps {
|
||||
entityData: GenericEntityProperties;
|
||||
urn: string;
|
||||
}
|
||||
|
||||
export function useHandleDeleteDomain({ entityData, urn }: DeleteDomainProps) {
|
||||
const client = useApolloClient();
|
||||
const { parentDomainsToUpdate, setParentDomainsToUpdate } = useDomainsContext();
|
||||
const { setDeletedDomain } = useDomainsContext();
|
||||
|
||||
const handleDeleteDomain = () => {
|
||||
if (entityData.parentDomains && entityData.parentDomains.domains.length > 0) {
|
||||
const parentDomainUrn = entityData.parentDomains.domains[0].urn;
|
||||
|
||||
removeFromListDomainsCache(client, urn, 1, 1000, parentDomainUrn);
|
||||
setParentDomainsToUpdate([...parentDomainsToUpdate, parentDomainUrn]);
|
||||
} else {
|
||||
removeFromListDomainsCache(client, urn, 1, 1000);
|
||||
}
|
||||
const parentDomainUrn = entityData?.parentDomains?.domains?.[0]?.urn || undefined;
|
||||
const deletedDomain: UpdatedDomain = {
|
||||
urn,
|
||||
type: EntityType.Domain,
|
||||
id: urn,
|
||||
parentDomain: parentDomainUrn,
|
||||
};
|
||||
setDeletedDomain(deletedDomain);
|
||||
};
|
||||
|
||||
return { handleDeleteDomain };
|
||||
|
||||
@ -1,21 +1,17 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { EventType } from '@app/analytics';
|
||||
import analytics from '@app/analytics/analytics';
|
||||
import { removeFromListDomainsCache, updateListDomainsCache } from '@app/domain/utils';
|
||||
import { useDomainsContext } from '@app/domainV2/DomainsContext';
|
||||
import { UpdatedDomain, useDomainsContext } from '@app/domainV2/DomainsContext';
|
||||
|
||||
import { Domain } from '@types';
|
||||
|
||||
export function useHandleMoveDomainComplete() {
|
||||
const client = useApolloClient();
|
||||
const { entityData } = useDomainsContext();
|
||||
const { entityData, setNewDomain, setDeletedDomain } = useDomainsContext();
|
||||
|
||||
const handleMoveDomainComplete = (urn: string, newParentUrn?: string) => {
|
||||
const handleMoveDomainComplete = (newParentUrn?: string) => {
|
||||
if (!entityData) return;
|
||||
|
||||
const domain = entityData as Domain;
|
||||
const oldParentUrn = domain.parentDomains?.domains?.[0].urn;
|
||||
const oldParentUrn = domain.parentDomains?.domains?.[0]?.urn;
|
||||
|
||||
analytics.event({
|
||||
type: EventType.MoveDomainEvent,
|
||||
@ -23,15 +19,17 @@ export function useHandleMoveDomainComplete() {
|
||||
parentDomainUrn: newParentUrn,
|
||||
});
|
||||
|
||||
removeFromListDomainsCache(client, urn, 1, 1000, oldParentUrn);
|
||||
updateListDomainsCache(
|
||||
client,
|
||||
domain.urn,
|
||||
undefined,
|
||||
domain.properties?.name ?? '',
|
||||
domain.properties?.description ?? '',
|
||||
newParentUrn,
|
||||
);
|
||||
const deletedDomain: UpdatedDomain = {
|
||||
...domain,
|
||||
parentDomain: oldParentUrn,
|
||||
};
|
||||
setDeletedDomain(deletedDomain);
|
||||
|
||||
const newDomain: UpdatedDomain = {
|
||||
...domain,
|
||||
parentDomain: newParentUrn,
|
||||
};
|
||||
setNewDomain(newDomain);
|
||||
};
|
||||
|
||||
return { handleMoveDomainComplete };
|
||||
|
||||
@ -5,6 +5,7 @@ import styled from 'styled-components/macro';
|
||||
|
||||
import colors from '@components/theme/foundations/colors';
|
||||
|
||||
import { useDomainsContext } from '@app/domainV2/DomainsContext';
|
||||
import { useEntityData, useRefetch } from '@app/entity/shared/EntityContext';
|
||||
import { useGlossaryEntityData } from '@app/entityV2/shared/GlossaryEntityContext';
|
||||
import { getParentNodeToUpdate, updateGlossarySidebar } from '@app/glossary/utils';
|
||||
@ -54,6 +55,7 @@ function EntityName(props: Props) {
|
||||
const refetch = useRefetch();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const { isInGlossaryContext, urnsToUpdate, setUrnsToUpdate } = useGlossaryEntityData();
|
||||
const { setUpdatedDomain } = useDomainsContext();
|
||||
const { urn, entityType, entityData } = useEntityData();
|
||||
const entityName = entityData ? entityRegistry.getDisplayName(entityType, entityData) : '';
|
||||
const [updatedName, setUpdatedName] = useState(entityName);
|
||||
@ -87,6 +89,17 @@ function EntityName(props: Props) {
|
||||
const parentNodeToUpdate = getParentNodeToUpdate(entityData, entityType);
|
||||
updateGlossarySidebar([parentNodeToUpdate], urnsToUpdate, setUrnsToUpdate);
|
||||
}
|
||||
if (setUpdatedDomain !== undefined) {
|
||||
const updatedDomain = {
|
||||
urn,
|
||||
type: EntityType.Domain,
|
||||
id: urn,
|
||||
properties: {
|
||||
name,
|
||||
},
|
||||
};
|
||||
setUpdatedDomain(updatedDomain);
|
||||
}
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
message.destroy();
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Empty, Form, Modal, Select, message } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import DomainNavigator from '@app/domain/nestedDomains/domainNavigator/DomainNavigator';
|
||||
import domainAutocompleteOptions from '@app/domainV2/DomainAutocompleteOptions';
|
||||
import DomainNavigator from '@app/domainV2/nestedDomains/domainNavigator/DomainNavigator';
|
||||
import { useEntityContext } from '@app/entity/shared/EntityContext';
|
||||
import { ANTD_GRAY } from '@app/entityV2/shared/constants';
|
||||
import { handleBatchError } from '@app/entityV2/shared/utils';
|
||||
@ -227,7 +227,7 @@ export const SetDomainModal = ({ urns, onCloseModal, refetch, defaultValue, onOk
|
||||
options={domainAutocompleteOptions(domainResult, searchLoading, entityRegistry)}
|
||||
/>
|
||||
<BrowserWrapper isHidden={!isShowingDomainNavigator}>
|
||||
<DomainNavigator selectDomainOverride={selectDomainFromBrowser} displayDomainColoredIcon />
|
||||
<DomainNavigator selectDomainOverride={selectDomainFromBrowser} />
|
||||
</BrowserWrapper>
|
||||
</ClickOutside>
|
||||
</Form.Item>
|
||||
|
||||
@ -4,7 +4,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
import DomainNavigator from '@app/domain/nestedDomains/domainNavigator/DomainNavigator';
|
||||
import DomainNavigator from '@app/domainV2/nestedDomains/domainNavigator/DomainNavigator';
|
||||
import { RESOURCE_TYPE, RESOURCE_URN, TYPE, URN } from '@app/permissions/policy/constants';
|
||||
import {
|
||||
EMPTY_POLICY,
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import PolicyPrivilegeForm from '@app/permissions/policy/PolicyPrivilegeForm';
|
||||
import * as policyUtils from '@app/permissions/policy/policyUtils';
|
||||
import { render } from '@utils/test-utils/customRender';
|
||||
|
||||
import { EntityType, PolicyMatchCondition, PolicyType, ResourceFilter } from '@types';
|
||||
|
||||
|
||||
@ -48,29 +48,51 @@ query getDomain($urn: String!) {
|
||||
}
|
||||
}
|
||||
|
||||
fragment ListDomain on Domain {
|
||||
urn
|
||||
id
|
||||
type
|
||||
properties {
|
||||
name
|
||||
description
|
||||
}
|
||||
parentDomains {
|
||||
...parentDomainsFields
|
||||
}
|
||||
ownership {
|
||||
...ownershipFields
|
||||
}
|
||||
displayProperties {
|
||||
...displayPropertiesFields
|
||||
}
|
||||
...domainEntitiesFields
|
||||
institutionalMemory {
|
||||
...institutionalMemoryFields
|
||||
}
|
||||
}
|
||||
|
||||
query listDomains($input: ListDomainsInput!) {
|
||||
listDomains(input: $input) {
|
||||
start
|
||||
count
|
||||
total
|
||||
domains {
|
||||
urn
|
||||
id
|
||||
type
|
||||
properties {
|
||||
name
|
||||
description
|
||||
...ListDomain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query scrollAcrossDomains($input: ScrollAcrossEntitiesInput!) {
|
||||
scrollAcrossEntities(input: $input) {
|
||||
nextScrollId
|
||||
count
|
||||
total
|
||||
searchResults {
|
||||
entity {
|
||||
... on Domain {
|
||||
...ListDomain
|
||||
}
|
||||
}
|
||||
parentDomains {
|
||||
...parentDomainsFields
|
||||
}
|
||||
ownership {
|
||||
...ownershipFields
|
||||
}
|
||||
displayProperties {
|
||||
...displayPropertiesFields
|
||||
}
|
||||
...domainEntitiesFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
datahub-web-react/src/utils/test-utils/customRender.tsx
Normal file
24
datahub-web-react/src/utils/test-utils/customRender.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { MockedProvider, MockedProviderProps } from '@apollo/client/testing';
|
||||
import { RenderOptions, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
// ApolloTestWrapper wraps children in MockedProvider with default settings
|
||||
export const ApolloTestWrapper: React.FC<Partial<MockedProviderProps> & { children: React.ReactNode }> = ({
|
||||
mocks = [],
|
||||
addTypename = false,
|
||||
children,
|
||||
...rest
|
||||
}) => (
|
||||
<MockedProvider mocks={mocks} addTypename={addTypename} {...rest}>
|
||||
{children}
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
// Custom render that always wraps with ApolloTestWrapper
|
||||
const customRender = (ui: React.ReactElement, options?: RenderOptions & { apolloMocks?: any[] }) =>
|
||||
render(<ApolloTestWrapper mocks={options?.apolloMocks}>{ui}</ApolloTestWrapper>, options);
|
||||
|
||||
// Re-export everything from @testing-library/react
|
||||
export * from '@testing-library/react';
|
||||
// Override render export
|
||||
export { customRender as render };
|
||||
Loading…
x
Reference in New Issue
Block a user